mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
sync
This commit is contained in:
parent
df4f65e4f0
commit
c9cbc2e0f0
4 changed files with 381 additions and 80 deletions
|
|
@ -1,43 +1,86 @@
|
|||
import { createStore } from "solid-js/store"
|
||||
import { For } from "solid-js"
|
||||
import { For, Match, Switch } from "solid-js"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
|
||||
const OPTIONS = {
|
||||
once: "Approve once",
|
||||
always: "Approve always",
|
||||
reject: "Reject",
|
||||
}
|
||||
const OPTION_LIST = Object.keys(OPTIONS)
|
||||
type Option = keyof typeof OPTIONS
|
||||
|
||||
export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
const sdk = useSDK()
|
||||
const { theme } = useTheme()
|
||||
const [store, setStore] = createStore({
|
||||
reply: "once" as Option,
|
||||
always: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={store.always}>
|
||||
<Prompt
|
||||
title="Always allow"
|
||||
body={props.request.always.join("\n").replaceAll("*", "")}
|
||||
options={{ confirm: "Confirm", cancel: "Cancel" }}
|
||||
onSelect={(option) => {
|
||||
if (option === "cancel") {
|
||||
setStore("always", false)
|
||||
return
|
||||
}
|
||||
sdk.client.permission.reply({
|
||||
reply: "always",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!store.always}>
|
||||
<Prompt
|
||||
title="Permission required"
|
||||
body={props.request.message}
|
||||
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
|
||||
onSelect={(option) => {
|
||||
if (option === "always") {
|
||||
setStore("always", true)
|
||||
return
|
||||
}
|
||||
sdk.client.permission.reply({
|
||||
reply: option as "once" | "reject",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Prompt<const T extends Record<string, string>>(props: {
|
||||
title: string
|
||||
body: string
|
||||
options: T
|
||||
onSelect: (option: keyof T) => void
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const keys = Object.keys(props.options) as (keyof T)[]
|
||||
const [store, setStore] = createStore({
|
||||
selected: keys[0],
|
||||
})
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "left") {
|
||||
const idx = OPTION_LIST.indexOf(store.reply)
|
||||
const next = OPTION_LIST[(idx - 1 + OPTION_LIST.length) % OPTION_LIST.length]
|
||||
setStore("reply", next as Option)
|
||||
if (evt.name === "left" || evt.name == "h") {
|
||||
evt.preventDefault()
|
||||
const idx = keys.indexOf(store.selected)
|
||||
const next = keys[(idx - 1 + keys.length) % keys.length]
|
||||
setStore("selected", next)
|
||||
}
|
||||
|
||||
if (evt.name === "right") {
|
||||
const idx = OPTION_LIST.indexOf(store.reply)
|
||||
const next = OPTION_LIST[(idx + 1) % OPTION_LIST.length]
|
||||
setStore("reply", next as Option)
|
||||
if (evt.name === "right" || evt.name == "l") {
|
||||
evt.preventDefault()
|
||||
const idx = keys.indexOf(store.selected)
|
||||
const next = keys[(idx + 1) % keys.length]
|
||||
setStore("selected", next)
|
||||
}
|
||||
|
||||
if (evt.name === "return") {
|
||||
sdk.client.permission.reply({
|
||||
reply: store.reply,
|
||||
requestID: props.request.id,
|
||||
})
|
||||
evt.preventDefault()
|
||||
props.onSelect(store.selected)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -46,11 +89,13 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
|||
<box gap={1} paddingLeft={3} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.warning}>{"△"}</text>
|
||||
<text fg={theme.text}>Permission required</text>
|
||||
<text fg={theme.text}>{props.title}</text>
|
||||
</box>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.textMuted}>{"→"}</text>
|
||||
<text fg={theme.textMuted}>{props.request.message}</text>
|
||||
<text fg={theme.textMuted} flexShrink={0}>
|
||||
{"→"}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{props.body}</text>
|
||||
</box>
|
||||
</box>
|
||||
<box
|
||||
|
|
@ -64,15 +109,15 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
|||
justifyContent="space-between"
|
||||
>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<For each={[...OPTION_LIST]}>
|
||||
<For each={keys}>
|
||||
{(option) => (
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={option === store.reply ? theme.primary : theme.backgroundMenu}
|
||||
backgroundColor={option === store.selected ? theme.primary : theme.backgroundMenu}
|
||||
>
|
||||
<text fg={option === store.reply ? theme.selectedListItemText : theme.textMuted}>
|
||||
{OPTIONS[option as Option]}
|
||||
<text fg={option === store.selected ? theme.selectedListItemText : theme.textMuted}>
|
||||
{props.options[option]}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { BusEvent } from "@/bus/bus-event"
|
|||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Log } from "@/util/log"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
|
|
@ -88,7 +89,10 @@ export namespace PermissionNext {
|
|||
),
|
||||
}
|
||||
|
||||
const state = Instance.state(() => {
|
||||
const state = Instance.state(async () => {
|
||||
const projectID = Instance.project.id
|
||||
const stored = await Storage.read<Ruleset>(["permission", projectID]).catch(() => [] as Ruleset)
|
||||
|
||||
const pending: Record<
|
||||
string,
|
||||
{
|
||||
|
|
@ -98,13 +102,9 @@ export namespace PermissionNext {
|
|||
}
|
||||
> = {}
|
||||
|
||||
const approved: {
|
||||
[projectID: string]: Set<string>
|
||||
} = {}
|
||||
|
||||
return {
|
||||
pending,
|
||||
approved,
|
||||
approved: stored,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -113,15 +113,15 @@ export namespace PermissionNext {
|
|||
ruleset: Ruleset,
|
||||
}),
|
||||
async (input) => {
|
||||
const s = await state()
|
||||
const { ruleset, ...request } = input
|
||||
for (const pattern of request.patterns ?? []) {
|
||||
const action = evaluate(request.permission, pattern, ruleset)
|
||||
const action = evaluate(request.permission, pattern, ruleset, s.approved)
|
||||
log.info("evaluated", { permission: request.permission, pattern, action })
|
||||
if (action === "deny") throw new RejectedError()
|
||||
if (action === "ask") {
|
||||
const id = input.id ?? Identifier.ascending("permission")
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const s = state()
|
||||
const info: Request = {
|
||||
id,
|
||||
...request,
|
||||
|
|
@ -145,7 +145,7 @@ export namespace PermissionNext {
|
|||
reply: Reply,
|
||||
}),
|
||||
async (input) => {
|
||||
const s = state()
|
||||
const s = await state()
|
||||
const existing = s.pending[input.requestID]
|
||||
if (!existing) return
|
||||
delete s.pending[input.requestID]
|
||||
|
|
@ -173,28 +173,28 @@ export namespace PermissionNext {
|
|||
}
|
||||
if (input.reply === "once") {
|
||||
existing.resolve()
|
||||
Bus.publish(Event.Replied, {
|
||||
sessionID: existing.info.sessionID,
|
||||
requestID: existing.info.id,
|
||||
reply: input.reply,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (input.reply === "always") {
|
||||
const projectID = Instance.project.id
|
||||
for (const pattern of existing.info.always) {
|
||||
s.approved.push({
|
||||
permission: existing.info.permission,
|
||||
pattern,
|
||||
action: "allow",
|
||||
})
|
||||
}
|
||||
await Storage.write(["permission", projectID], s.approved)
|
||||
existing.resolve()
|
||||
Bus.publish(Event.Replied, {
|
||||
sessionID: existing.info.sessionID,
|
||||
requestID: existing.info.id,
|
||||
reply: input.reply,
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function evaluate(permission: string, pattern: string, ruleset: Ruleset): Action {
|
||||
log.info("evaluate", { permission, pattern, ruleset })
|
||||
const match = ruleset.findLast(
|
||||
export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Action {
|
||||
const merged = merge(...rulesets)
|
||||
log.info("evaluate", { permission, pattern, ruleset: merged })
|
||||
const match = merged.findLast(
|
||||
(rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern),
|
||||
)
|
||||
return match?.action ?? "ask"
|
||||
|
|
@ -203,14 +203,14 @@ export namespace PermissionNext {
|
|||
const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"]
|
||||
|
||||
export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
|
||||
const disabled = new Set<string>()
|
||||
const result = new Set<string>()
|
||||
for (const tool of tools) {
|
||||
const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool
|
||||
if (evaluate(permission, "*", ruleset) === "deny") {
|
||||
disabled.add(tool)
|
||||
result.add(tool)
|
||||
}
|
||||
}
|
||||
return disabled
|
||||
return result
|
||||
}
|
||||
|
||||
export class RejectedError extends Error {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { fileURLToPath } from "url"
|
|||
import { Flag } from "@/flag/flag.ts"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
import { BashArity } from "@/permission/arity"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
|
@ -81,24 +82,11 @@ export const BashTool = Tool.define("bash", async () => {
|
|||
}
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
|
||||
const checkExternalDirectory = async (dir: string) => {
|
||||
if (Filesystem.contains(Instance.directory, dir)) return
|
||||
await PermissionNext.ask({
|
||||
permission: "external_directory",
|
||||
message: `This command references paths outside of ${Instance.directory}`,
|
||||
patterns: [dir],
|
||||
always: [dir + "*"],
|
||||
sessionID: ctx.sessionID,
|
||||
metadata: {},
|
||||
ruleset: agent.permission,
|
||||
})
|
||||
}
|
||||
const directories = new Set<string>()
|
||||
if (!Filesystem.contains(Instance.directory, cwd)) directories.add(cwd)
|
||||
const patterns = new Set<string>()
|
||||
const always = new Set<string>()
|
||||
|
||||
await checkExternalDirectory(cwd)
|
||||
|
||||
const permissions = agent.permission.bash
|
||||
|
||||
const askPatterns = new Set<string>()
|
||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||
if (!node) continue
|
||||
const command = []
|
||||
|
|
@ -133,27 +121,39 @@ export const BashTool = Tool.define("bash", async () => {
|
|||
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
|
||||
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
|
||||
: resolved
|
||||
|
||||
await checkExternalDirectory(normalized)
|
||||
directories.add(normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// always allow cd if it passes above check
|
||||
// cd covered by above check
|
||||
if (command.length && command[0] !== "cd") {
|
||||
askPatterns.add(command.join(" "))
|
||||
patterns.add(command.join(" "))
|
||||
always.add(BashArity.prefix(command).join(" ") + "*")
|
||||
}
|
||||
}
|
||||
|
||||
if (askPatterns.size > 0) {
|
||||
const patterns = Array.from(askPatterns)
|
||||
if (directories.size > 0) {
|
||||
const dirs = Array.from(directories)
|
||||
await PermissionNext.ask({
|
||||
permission: "external_directory",
|
||||
message: `Requesting access to external directories: ${dirs.join(", ")}`,
|
||||
patterns: Array.from(directories),
|
||||
always: Array.from(directories).map((x) => x + "*"),
|
||||
sessionID: ctx.sessionID,
|
||||
metadata: {},
|
||||
ruleset: agent.permission,
|
||||
})
|
||||
}
|
||||
|
||||
if (patterns.size > 0) {
|
||||
await PermissionNext.ask({
|
||||
permission: "bash",
|
||||
patterns,
|
||||
patterns: Array.from(patterns),
|
||||
always: Array.from(always),
|
||||
sessionID: ctx.sessionID,
|
||||
message: params.command,
|
||||
metadata: {},
|
||||
always: ["*"],
|
||||
ruleset: agent.permission,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { PermissionNext } from "../../src/permission/next"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Storage } from "../../src/storage/storage"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
// fromConfig tests
|
||||
|
||||
|
|
@ -297,6 +300,14 @@ test("evaluate - permission patterns sorted by length regardless of object order
|
|||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - merges multiple rulesets", () => {
|
||||
const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const approved: PermissionNext.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
|
||||
// approved comes after config, so rm should be denied
|
||||
const result = PermissionNext.evaluate("bash", "rm", config, approved)
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
// disabled tests
|
||||
|
||||
test("disabled - returns empty set when all tools allowed", () => {
|
||||
|
|
@ -405,3 +416,248 @@ test("disabled - specific allow overrides wildcard deny", () => {
|
|||
expect(result.has("edit")).toBe(true)
|
||||
expect(result.has("read")).toBe(true)
|
||||
})
|
||||
|
||||
// ask tests
|
||||
|
||||
test("ask - resolves immediately when action is allow", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls command",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
|
||||
})
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("ask - throws RejectedError when action is deny", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(
|
||||
PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["rm -rf /"],
|
||||
message: "Run dangerous command",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(PermissionNext.RejectedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("ask - returns pending promise when action is ask", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const promise = PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls command",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
|
||||
})
|
||||
// Promise should be pending, not resolved
|
||||
expect(promise).toBeInstanceOf(Promise)
|
||||
// Don't await - just verify it returns a promise
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// reply tests
|
||||
|
||||
test("reply - once resolves the pending ask", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_test1",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls command",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
await PermissionNext.reply({
|
||||
requestID: "permission_test1",
|
||||
reply: "once",
|
||||
})
|
||||
|
||||
await expect(askPromise).resolves.toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("reply - reject throws RejectedError", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_test2",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls command",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
await PermissionNext.reply({
|
||||
requestID: "permission_test2",
|
||||
reply: "reject",
|
||||
})
|
||||
|
||||
await expect(askPromise).rejects.toBeInstanceOf(PermissionNext.RejectedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("reply - always persists approval and resolves", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_test3",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls command",
|
||||
metadata: {},
|
||||
always: ["ls"],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
await PermissionNext.reply({
|
||||
requestID: "permission_test3",
|
||||
reply: "always",
|
||||
})
|
||||
|
||||
await expect(askPromise).resolves.toBeUndefined()
|
||||
},
|
||||
})
|
||||
// Re-provide to reload state with stored permissions
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Stored approval should allow without asking
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test2",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls command",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("reply - reject cancels all pending for same session", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise1 = PermissionNext.ask({
|
||||
id: "permission_test4a",
|
||||
sessionID: "session_same",
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
message: "Run ls",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
const askPromise2 = PermissionNext.ask({
|
||||
id: "permission_test4b",
|
||||
sessionID: "session_same",
|
||||
permission: "edit",
|
||||
patterns: ["foo.ts"],
|
||||
message: "Edit file",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
// Catch rejections before they become unhandled
|
||||
const result1 = askPromise1.catch((e) => e)
|
||||
const result2 = askPromise2.catch((e) => e)
|
||||
|
||||
// Reject the first one
|
||||
await PermissionNext.reply({
|
||||
requestID: "permission_test4a",
|
||||
reply: "reject",
|
||||
})
|
||||
|
||||
// Both should be rejected
|
||||
expect(await result1).toBeInstanceOf(PermissionNext.RejectedError)
|
||||
expect(await result2).toBeInstanceOf(PermissionNext.RejectedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("ask - checks all patterns and stops on first deny", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(
|
||||
PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["echo hello", "rm -rf /"],
|
||||
message: "Run commands",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm *", action: "deny" },
|
||||
],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(PermissionNext.RejectedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("ask - allows all patterns when all match allow rules", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["echo hello", "ls -la", "pwd"],
|
||||
message: "Run safe commands",
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
|
||||
})
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue