Files
Kilo-Org_kilocode/packages/opencode/test/permission/next.test.ts
T
Imanol Maiztegui 251ed67347 refactor(opencode): update internal import paths to use barrel re-exports
Migrate all deep import paths across source and test files to
reference barrel index modules instead of direct file paths. This
covers config, provider, storage, util, tool, lsp, project, and
installation modules. Also moves provider promise helpers out of the
namespace block, switches Clipboard to namespace import style, fixes a
duplicate Filesystem import, removes the deleted paste-summary test,
and corrects snapshot cache typing and indentation.
2026-04-22 07:24:09 +02:00

1156 lines
37 KiB
TypeScript

import { afterAll, afterEach, test, expect } from "bun:test" // kilocode_change
import fs from "fs/promises" // kilocode_change
import os from "os"
import path from "path" // kilocode_change
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config" // kilocode_change
import { Global } from "../../src/global" // kilocode_change
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Permission } from "../../src/permission"
import { PermissionID } from "../../src/permission/schema"
import { Instance } from "../../src/project/instance"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { MessageID, SessionID } from "../../src/session/schema"
const bus = Bus.layer
const env = Layer.mergeAll(Permission.layer.pipe(Layer.provide(bus)), bus, CrossSpawnSpawner.defaultLayer)
const it = testEffect(env)
afterEach(async () => {
await Instance.disposeAll()
})
// kilocode_change start
afterAll(async () => {
const dir = Global.Path.config
for (const file of ["kilo.jsonc", "kilo.json", "config.json", "opencode.json", "opencode.jsonc"]) {
await fs.rm(path.join(dir, file), { force: true }).catch(() => {})
}
await Config.invalidate(true)
})
// kilocode_change end
const rejectAll = (message?: string) =>
Effect.gen(function* () {
const permission = yield* Permission.Service
for (const req of yield* permission.list()) {
yield* permission.reply({
requestID: req.id,
reply: "reject",
message,
})
}
})
const waitForPending = (count: number) =>
Effect.gen(function* () {
const permission = yield* Permission.Service
for (let i = 0; i < 100; i++) {
const list = yield* permission.list()
if (list.length === count) return list
yield* Effect.sleep("10 millis")
}
return yield* Effect.fail(new Error(`timed out waiting for ${count} pending permission request(s)`))
})
const fail = <A, E, R>(self: Effect.Effect<A, E, R>) =>
Effect.gen(function* () {
const exit = yield* self.pipe(Effect.exit)
if (Exit.isFailure(exit)) return Cause.squash(exit.cause)
throw new Error("expected permission effect to fail")
})
const ask = (input: Parameters<Permission.Interface["ask"]>[0]) =>
Effect.gen(function* () {
const permission = yield* Permission.Service
return yield* permission.ask(input)
})
const reply = (input: Parameters<Permission.Interface["reply"]>[0]) =>
Effect.gen(function* () {
const permission = yield* Permission.Service
return yield* permission.reply(input)
})
const list = () =>
Effect.gen(function* () {
const permission = yield* Permission.Service
return yield* permission.list()
})
function withDir(options: { git?: boolean } | undefined, self: (dir: string) => Effect.Effect<any, any, any>) {
return provideTmpdirInstance(self, options)
}
function withProvided(dir: string) {
return <A, E, R>(self: Effect.Effect<A, E, R>) => self.pipe(provideInstance(dir))
}
// fromConfig tests
test("fromConfig - string value becomes wildcard rule", () => {
const result = Permission.fromConfig({ bash: "allow" })
expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
})
test("fromConfig - object value converts to rules array", () => {
const result = Permission.fromConfig({ bash: { "*": "allow", rm: "deny" } })
expect(result).toEqual([
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "rm", action: "deny" },
])
})
test("fromConfig - mixed string and object values", () => {
const result = Permission.fromConfig({
bash: { "*": "allow", rm: "deny" },
edit: "allow",
webfetch: "ask",
})
expect(result).toEqual([
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "rm", action: "deny" },
{ permission: "edit", pattern: "*", action: "allow" },
{ permission: "webfetch", pattern: "*", action: "ask" },
])
})
test("fromConfig - empty object", () => {
const result = Permission.fromConfig({})
expect(result).toEqual([])
})
test("fromConfig - expands tilde to home directory", () => {
const result = Permission.fromConfig({ external_directory: { "~/projects/*": "allow" } })
expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }])
})
test("fromConfig - expands $HOME to home directory", () => {
const result = Permission.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } })
expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }])
})
test("fromConfig - expands $HOME without trailing slash", () => {
const result = Permission.fromConfig({ external_directory: { $HOME: "allow" } })
expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }])
})
test("fromConfig - does not expand tilde in middle of path", () => {
const result = Permission.fromConfig({ external_directory: { "/some/~/path": "allow" } })
expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }])
})
test("fromConfig - expands exact tilde to home directory", () => {
const result = Permission.fromConfig({ external_directory: { "~": "allow" } })
expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }])
})
test("evaluate - matches expanded tilde pattern", () => {
const ruleset = Permission.fromConfig({ external_directory: { "~/projects/*": "allow" } })
const result = Permission.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset)
expect(result.action).toBe("allow")
})
test("evaluate - matches expanded $HOME pattern", () => {
const ruleset = Permission.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } })
const result = Permission.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset)
expect(result.action).toBe("allow")
})
// merge tests
test("merge - simple concatenation", () => {
const result = Permission.merge(
[{ permission: "bash", pattern: "*", action: "allow" }],
[{ permission: "bash", pattern: "*", action: "deny" }],
)
expect(result).toEqual([
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "*", action: "deny" },
])
})
test("merge - adds new permission", () => {
const result = Permission.merge(
[{ permission: "bash", pattern: "*", action: "allow" }],
[{ permission: "edit", pattern: "*", action: "deny" }],
)
expect(result).toEqual([
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "edit", pattern: "*", action: "deny" },
])
})
test("merge - concatenates rules for same permission", () => {
const result = Permission.merge(
[{ permission: "bash", pattern: "foo", action: "ask" }],
[{ permission: "bash", pattern: "*", action: "deny" }],
)
expect(result).toEqual([
{ permission: "bash", pattern: "foo", action: "ask" },
{ permission: "bash", pattern: "*", action: "deny" },
])
})
test("merge - multiple rulesets", () => {
const result = Permission.merge(
[{ permission: "bash", pattern: "*", action: "allow" }],
[{ permission: "bash", pattern: "rm", action: "ask" }],
[{ permission: "edit", pattern: "*", action: "allow" }],
)
expect(result).toEqual([
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "rm", action: "ask" },
{ permission: "edit", pattern: "*", action: "allow" },
])
})
test("merge - empty ruleset does nothing", () => {
const result = Permission.merge([{ permission: "bash", pattern: "*", action: "allow" }], [])
expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
})
test("merge - preserves rule order", () => {
const result = Permission.merge(
[
{ permission: "edit", pattern: "src/*", action: "allow" },
{ permission: "edit", pattern: "src/secret/*", action: "deny" },
],
[{ permission: "edit", pattern: "src/secret/ok.ts", action: "allow" }],
)
expect(result).toEqual([
{ permission: "edit", pattern: "src/*", action: "allow" },
{ permission: "edit", pattern: "src/secret/*", action: "deny" },
{ permission: "edit", pattern: "src/secret/ok.ts", action: "allow" },
])
})
test("merge - config permission overrides default ask", () => {
const defaults: Permission.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
const config: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const merged = Permission.merge(defaults, config)
expect(Permission.evaluate("bash", "ls", merged).action).toBe("allow")
expect(Permission.evaluate("edit", "foo.ts", merged).action).toBe("ask")
})
test("merge - config ask overrides default allow", () => {
const defaults: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const config: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
const merged = Permission.merge(defaults, config)
expect(Permission.evaluate("bash", "ls", merged).action).toBe("ask")
})
// evaluate tests
test("evaluate - exact pattern match", () => {
const result = Permission.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }])
expect(result.action).toBe("deny")
})
test("evaluate - wildcard pattern match", () => {
const result = Permission.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }])
expect(result.action).toBe("allow")
})
test("evaluate - last matching rule wins", () => {
const result = Permission.evaluate("bash", "rm", [
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "rm", action: "deny" },
])
expect(result.action).toBe("deny")
})
test("evaluate - last matching rule wins (wildcard after specific)", () => {
const result = Permission.evaluate("bash", "rm", [
{ permission: "bash", pattern: "rm", action: "deny" },
{ permission: "bash", pattern: "*", action: "allow" },
])
expect(result.action).toBe("allow")
})
test("evaluate - glob pattern match", () => {
const result = Permission.evaluate("edit", "src/foo.ts", [{ permission: "edit", pattern: "src/*", action: "allow" }])
expect(result.action).toBe("allow")
})
test("evaluate - last matching glob wins", () => {
const result = Permission.evaluate("edit", "src/components/Button.tsx", [
{ permission: "edit", pattern: "src/*", action: "deny" },
{ permission: "edit", pattern: "src/components/*", action: "allow" },
])
expect(result.action).toBe("allow")
})
test("evaluate - order matters for specificity", () => {
const result = Permission.evaluate("edit", "src/components/Button.tsx", [
{ permission: "edit", pattern: "src/components/*", action: "allow" },
{ permission: "edit", pattern: "src/*", action: "deny" },
])
expect(result.action).toBe("deny")
})
test("evaluate - unknown permission returns ask", () => {
const result = Permission.evaluate("unknown_tool", "anything", [
{ permission: "bash", pattern: "*", action: "allow" },
])
expect(result.action).toBe("ask")
})
test("evaluate - empty ruleset returns ask", () => {
const result = Permission.evaluate("bash", "rm", [])
expect(result.action).toBe("ask")
})
test("evaluate - no matching pattern returns ask", () => {
const result = Permission.evaluate("edit", "etc/passwd", [{ permission: "edit", pattern: "src/*", action: "allow" }])
expect(result.action).toBe("ask")
})
test("evaluate - empty rules array returns ask", () => {
const result = Permission.evaluate("bash", "rm", [])
expect(result.action).toBe("ask")
})
test("evaluate - multiple matching patterns, last wins", () => {
const result = Permission.evaluate("edit", "src/secret.ts", [
{ permission: "edit", pattern: "*", action: "ask" },
{ permission: "edit", pattern: "src/*", action: "allow" },
{ permission: "edit", pattern: "src/secret.ts", action: "deny" },
])
expect(result.action).toBe("deny")
})
test("evaluate - non-matching patterns are skipped", () => {
const result = Permission.evaluate("edit", "src/foo.ts", [
{ permission: "edit", pattern: "*", action: "ask" },
{ permission: "edit", pattern: "test/*", action: "deny" },
{ permission: "edit", pattern: "src/*", action: "allow" },
])
expect(result.action).toBe("allow")
})
test("evaluate - exact match at end wins over earlier wildcard", () => {
const result = Permission.evaluate("bash", "/bin/rm", [
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "/bin/rm", action: "deny" },
])
expect(result.action).toBe("deny")
})
test("evaluate - wildcard at end overrides earlier exact match", () => {
const result = Permission.evaluate("bash", "/bin/rm", [
{ permission: "bash", pattern: "/bin/rm", action: "deny" },
{ permission: "bash", pattern: "*", action: "allow" },
])
expect(result.action).toBe("allow")
})
// wildcard permission tests
test("evaluate - wildcard permission matches any permission", () => {
const result = Permission.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }])
expect(result.action).toBe("deny")
})
test("evaluate - wildcard permission with specific pattern", () => {
const result = Permission.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }])
expect(result.action).toBe("deny")
})
test("evaluate - glob permission pattern", () => {
const result = Permission.evaluate("mcp_server_tool", "anything", [
{ permission: "mcp_*", pattern: "*", action: "allow" },
])
expect(result.action).toBe("allow")
})
test("evaluate - specific permission and wildcard permission combined", () => {
const result = Permission.evaluate("bash", "rm", [
{ permission: "*", pattern: "*", action: "deny" },
{ permission: "bash", pattern: "*", action: "allow" },
])
expect(result.action).toBe("allow")
})
test("evaluate - wildcard permission does not match when specific exists", () => {
const result = Permission.evaluate("edit", "src/foo.ts", [
{ permission: "*", pattern: "*", action: "deny" },
{ permission: "edit", pattern: "src/*", action: "allow" },
])
expect(result.action).toBe("allow")
})
test("evaluate - multiple matching permission patterns combine rules", () => {
const result = Permission.evaluate("mcp_dangerous", "anything", [
{ permission: "*", pattern: "*", action: "ask" },
{ permission: "mcp_*", pattern: "*", action: "allow" },
{ permission: "mcp_dangerous", pattern: "*", action: "deny" },
])
expect(result.action).toBe("deny")
})
test("evaluate - wildcard permission fallback for unknown tool", () => {
const result = Permission.evaluate("unknown_tool", "anything", [
{ permission: "*", pattern: "*", action: "ask" },
{ permission: "bash", pattern: "*", action: "allow" },
])
expect(result.action).toBe("ask")
})
test("evaluate - permission patterns sorted by length regardless of object order", () => {
const result = Permission.evaluate("bash", "rm", [
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "*", pattern: "*", action: "deny" },
])
expect(result.action).toBe("deny")
})
test("evaluate - merges multiple rulesets", () => {
const config: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const approved: Permission.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
const result = Permission.evaluate("bash", "rm", config, approved)
expect(result.action).toBe("deny")
})
// disabled tests
test("disabled - returns empty set when all tools allowed", () => {
const result = Permission.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }])
expect(result.size).toBe(0)
})
test("disabled - disables tool when denied", () => {
const result = Permission.disabled(
["bash", "edit", "read"],
[
{ permission: "*", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "*", action: "deny" },
],
)
expect(result.has("bash")).toBe(true)
expect(result.has("edit")).toBe(false)
expect(result.has("read")).toBe(false)
})
test("disabled - disables edit/write/apply_patch/multiedit when edit denied", () => {
const result = Permission.disabled(
["edit", "write", "apply_patch", "multiedit", "bash"],
[
{ permission: "*", pattern: "*", action: "allow" },
{ permission: "edit", pattern: "*", action: "deny" },
],
)
expect(result.has("edit")).toBe(true)
expect(result.has("write")).toBe(true)
expect(result.has("apply_patch")).toBe(true)
expect(result.has("multiedit")).toBe(true)
expect(result.has("bash")).toBe(false)
})
test("disabled - does not disable when partially denied", () => {
const result = Permission.disabled(
["bash"],
[
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "rm *", action: "deny" },
],
)
expect(result.has("bash")).toBe(false)
})
test("disabled - does not disable when action is ask", () => {
const result = Permission.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }])
expect(result.size).toBe(0)
})
test("disabled - does not disable when specific allow after wildcard deny", () => {
const result = Permission.disabled(
["bash"],
[
{ permission: "bash", pattern: "*", action: "deny" },
{ permission: "bash", pattern: "echo *", action: "allow" },
],
)
expect(result.has("bash")).toBe(false)
})
test("disabled - does not disable when wildcard allow after deny", () => {
const result = Permission.disabled(
["bash"],
[
{ permission: "bash", pattern: "rm *", action: "deny" },
{ permission: "bash", pattern: "*", action: "allow" },
],
)
expect(result.has("bash")).toBe(false)
})
test("disabled - disables multiple tools", () => {
const result = Permission.disabled(
["bash", "edit", "webfetch"],
[
{ permission: "bash", pattern: "*", action: "deny" },
{ permission: "edit", pattern: "*", action: "deny" },
{ permission: "webfetch", pattern: "*", action: "deny" },
],
)
expect(result.has("bash")).toBe(true)
expect(result.has("edit")).toBe(true)
expect(result.has("webfetch")).toBe(true)
})
test("disabled - wildcard permission denies all tools", () => {
const result = Permission.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }])
expect(result.has("bash")).toBe(true)
expect(result.has("edit")).toBe(true)
expect(result.has("read")).toBe(true)
})
test("disabled - specific allow overrides wildcard deny", () => {
const result = Permission.disabled(
["bash", "edit", "read"],
[
{ permission: "*", pattern: "*", action: "deny" },
{ permission: "bash", pattern: "*", action: "allow" },
],
)
expect(result.has("bash")).toBe(false)
expect(result.has("edit")).toBe(true)
expect(result.has("read")).toBe(true)
})
// ask tests
it.live("ask - resolves immediately when action is allow", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const result = yield* ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
})
expect(result).toBeUndefined()
}),
),
)
it.live("ask - throws DeniedError when action is deny", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const err = yield* fail(
ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["rm -rf /"],
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
}),
)
expect(err).toBeInstanceOf(Permission.DeniedError)
}),
),
)
it.live("ask - stays pending when action is ask", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const fiber = yield* ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
}).pipe(Effect.forkScoped)
expect(yield* waitForPending(1)).toHaveLength(1)
yield* rejectAll()
yield* Fiber.await(fiber)
}),
),
)
it.live("ask - adds request to pending list", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const fiber = yield* ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: { cmd: "ls" },
always: ["ls"],
tool: {
messageID: MessageID.make("msg_test"),
callID: "call_test",
},
ruleset: [],
}).pipe(Effect.forkScoped)
const items = yield* waitForPending(1)
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: { cmd: "ls" },
always: ["ls"],
tool: {
messageID: MessageID.make("msg_test"),
callID: "call_test",
},
})
yield* rejectAll()
yield* Fiber.await(fiber)
}),
),
)
it.live("ask - publishes asked event", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
let seen: Permission.Request | undefined
const unsub = yield* bus.subscribeCallback(Permission.Event.Asked, (event) => {
seen = event.properties
})
try {
const fiber = yield* ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: { cmd: "ls" },
always: ["ls"],
tool: {
messageID: MessageID.make("msg_test"),
callID: "call_test",
},
ruleset: [],
}).pipe(Effect.forkScoped)
expect(yield* waitForPending(1)).toHaveLength(1)
expect(seen).toBeDefined()
expect(seen).toMatchObject({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
})
yield* rejectAll()
yield* Fiber.await(fiber)
} finally {
unsub()
}
}),
),
)
// reply tests
it.live("reply - once resolves the pending ask", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionID.make("per_test1"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({ requestID: PermissionID.make("per_test1"), reply: "once" })
yield* Fiber.join(fiber)
}),
),
)
it.live("reply - reject throws RejectedError", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionID.make("per_test2"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({ requestID: PermissionID.make("per_test2"), reply: "reject" })
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
}),
),
)
it.live("reply - reject with message throws CorrectedError", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionID.make("per_test2b"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({
requestID: PermissionID.make("per_test2b"),
reply: "reject",
message: "Use a safer command",
})
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const err = Cause.squash(exit.cause)
expect(err).toBeInstanceOf(Permission.CorrectedError)
expect(String(err)).toContain("Use a safer command")
}
}),
),
)
it.live("reply - always persists approval and resolves", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const run = withProvided(dir)
const fiber = yield* ask({
id: PermissionID.make("per_test3"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: ["ls"],
ruleset: [],
}).pipe(run, Effect.forkScoped)
yield* waitForPending(1).pipe(run)
yield* reply({ requestID: PermissionID.make("per_test3"), reply: "always" }).pipe(run)
yield* Fiber.join(fiber)
const result = yield* ask({
sessionID: SessionID.make("session_test2"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(run)
expect(result).toBeUndefined()
}),
)
// kilocode_change start - session-scoped allowEverything enable
it.live("allowEverything - session-scoped enable stays within one session", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const permission = yield* Permission.Service
const first = yield* ask({
id: PermissionID.make("permission_session_allow"),
sessionID: SessionID.make("session_allowed"),
permission: "bash",
patterns: ["pwd"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* permission.allowEverything({
enable: true,
requestID: "permission_session_allow",
sessionID: "session_allowed",
})
yield* Fiber.join(first)
const allowed = yield* ask({
sessionID: SessionID.make("session_allowed"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
})
expect(allowed).toBeUndefined()
const blocked = yield* ask({
id: PermissionID.make("permission_session_blocked"),
sessionID: SessionID.make("session_blocked"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({
requestID: PermissionID.make("permission_session_blocked"),
reply: "reject",
})
const exit = yield* Fiber.await(blocked)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
}
}),
),
)
// kilocode_change end
it.live("reply - reject cancels all pending for same session", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionID.make("per_test4a"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
const b = yield* ask({
id: PermissionID.make("per_test4b"),
sessionID: SessionID.make("session_same"),
permission: "edit",
patterns: ["foo.ts"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(2)
yield* reply({ requestID: PermissionID.make("per_test4a"), reply: "reject" })
const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
expect(Exit.isFailure(ea)).toBe(true)
expect(Exit.isFailure(eb)).toBe(true)
if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(Permission.RejectedError)
if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(Permission.RejectedError)
}),
),
)
it.live("reply - always resolves matching pending requests in same session", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionID.make("per_test5a"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: ["ls"],
ruleset: [],
}).pipe(Effect.forkScoped)
const b = yield* ask({
id: PermissionID.make("per_test5b"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(2)
yield* reply({ requestID: PermissionID.make("per_test5a"), reply: "always" })
yield* Fiber.join(a)
yield* Fiber.join(b)
expect(yield* list()).toHaveLength(0)
}),
),
)
it.live("reply - always keeps other session pending", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionID.make("per_test6a"),
sessionID: SessionID.make("session_a"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: ["ls"],
ruleset: [],
}).pipe(Effect.forkScoped)
const b = yield* ask({
id: PermissionID.make("per_test6b"),
sessionID: SessionID.make("session_b"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(2)
yield* reply({ requestID: PermissionID.make("per_test6a"), reply: "always" })
yield* Fiber.join(a)
expect((yield* list()).map((item) => item.id)).toEqual([PermissionID.make("per_test6b")])
yield* rejectAll()
yield* Fiber.await(b)
}),
),
)
it.live("reply - publishes replied event", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
let resolve!: (value: { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }) => void
const seen = Effect.promise<{
sessionID: SessionID
requestID: PermissionID
reply: Permission.Reply
}>(
() =>
new Promise((res) => {
resolve = res
}),
)
const fiber = yield* ask({
id: PermissionID.make("per_test7"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
const unsub = yield* bus.subscribeCallback(Permission.Event.Replied, (event) => {
resolve(event.properties)
})
try {
yield* reply({ requestID: PermissionID.make("per_test7"), reply: "once" })
yield* Fiber.join(fiber)
expect(yield* seen).toEqual({
sessionID: SessionID.make("session_test"),
requestID: PermissionID.make("per_test7"),
reply: "once",
})
} finally {
unsub()
}
}),
),
)
it.live("permission requests stay isolated by directory", () =>
Effect.gen(function* () {
const one = yield* tmpdirScoped({ git: true })
const two = yield* tmpdirScoped({ git: true })
const runOne = withProvided(one)
const runTwo = withProvided(two)
const a = yield* ask({
id: PermissionID.make("per_dir_a"),
sessionID: SessionID.make("session_dir_a"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(runOne, Effect.forkScoped)
const b = yield* ask({
id: PermissionID.make("per_dir_b"),
sessionID: SessionID.make("session_dir_b"),
permission: "bash",
patterns: ["pwd"],
metadata: {},
always: [],
ruleset: [],
}).pipe(runTwo, Effect.forkScoped)
const onePending = yield* waitForPending(1).pipe(runOne)
const twoPending = yield* waitForPending(1).pipe(runTwo)
expect(onePending).toHaveLength(1)
expect(twoPending).toHaveLength(1)
expect(onePending[0].id).toBe(PermissionID.make("per_dir_a"))
expect(twoPending[0].id).toBe(PermissionID.make("per_dir_b"))
yield* reply({ requestID: onePending[0].id, reply: "reject" }).pipe(runOne)
yield* reply({ requestID: twoPending[0].id, reply: "reject" }).pipe(runTwo)
yield* Fiber.await(a)
yield* Fiber.await(b)
}),
)
it.live("pending permission rejects on instance dispose", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const run = withProvided(dir)
const fiber = yield* ask({
id: PermissionID.make("per_dispose"),
sessionID: SessionID.make("session_dispose"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(run, Effect.forkScoped)
expect(yield* waitForPending(1).pipe(run)).toHaveLength(1)
yield* Effect.promise(() => Instance.provide({ directory: dir, fn: () => void Instance.dispose() }))
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
}),
)
it.live("pending permission rejects on instance reload", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const run = withProvided(dir)
const fiber = yield* ask({
id: PermissionID.make("per_reload"),
sessionID: SessionID.make("session_reload"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(run, Effect.forkScoped)
expect(yield* waitForPending(1).pipe(run)).toHaveLength(1)
yield* Effect.promise(() => Instance.reload({ directory: dir }))
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
}),
)
it.live("reply - does nothing for unknown requestID", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
yield* reply({ requestID: PermissionID.make("per_unknown"), reply: "once" })
expect(yield* list()).toHaveLength(0)
}),
),
)
it.live("ask - checks all patterns and stops on first deny", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const err = yield* fail(
ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["echo hello", "rm -rf /"],
metadata: {},
always: [],
ruleset: [
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "rm *", action: "deny" },
],
}),
)
expect(err).toBeInstanceOf(Permission.DeniedError)
}),
),
)
it.live("ask - allows all patterns when all match allow rules", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const result = yield* ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["echo hello", "ls -la", "pwd"],
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
})
expect(result).toBeUndefined()
}),
),
)
it.live("ask - should deny even when an earlier pattern is ask", () =>
withDir({ git: true }, () =>
Effect.gen(function* () {
const err = yield* fail(
ask({
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["echo hello", "rm -rf /"],
metadata: {},
always: [],
ruleset: [
{ permission: "bash", pattern: "echo *", action: "ask" },
{ permission: "bash", pattern: "rm *", action: "deny" },
],
}),
)
expect(err).toBeInstanceOf(Permission.DeniedError)
expect(yield* list()).toHaveLength(0)
}),
),
)
it.live("ask - abort should clear pending request", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const run = withProvided(dir)
const fiber = yield* ask({
id: PermissionID.make("per_reload"),
sessionID: SessionID.make("session_reload"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
}).pipe(run, Effect.forkScoped)
const pending = yield* waitForPending(1).pipe(run)
expect(pending).toHaveLength(1)
yield* Effect.promise(() => Instance.reload({ directory: dir }))
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
}),
)