fix(core): enforce V2 tool permissions (#31061)
This commit is contained in:
@@ -219,7 +219,7 @@ export const layer = Layer.effect(
|
|||||||
.filter((part): part is string => part !== undefined && part.length > 0)
|
.filter((part): part is string => part !== undefined && part.length > 0)
|
||||||
.map(SystemPart.make),
|
.map(SystemPart.make),
|
||||||
messages: toLLMMessages(context, model),
|
messages: toLLMMessages(context, model),
|
||||||
tools: yield* tools.definitions(),
|
tools: yield* tools.definitions(agent.info?.permissions),
|
||||||
})
|
})
|
||||||
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
|
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
|
||||||
return yield* Effect.die(rebuildPreparedTurn())
|
return yield* Effect.die(rebuildPreparedTurn())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as QuestionTool from "./question"
|
export * as QuestionTool from "./question"
|
||||||
|
|
||||||
import { Tool, toolText } from "@opencode-ai/llm"
|
import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { QuestionV2 } from "../question"
|
import { QuestionV2 } from "../question"
|
||||||
import { ToolRegistry } from "./registry"
|
import { ToolRegistry } from "./registry"
|
||||||
@@ -57,6 +57,11 @@ export const layer = Layer.effectDiscard(
|
|||||||
yield* registry.contribute((editor) =>
|
yield* registry.contribute((editor) =>
|
||||||
editor.set(name, {
|
editor.set(name, {
|
||||||
tool: definition,
|
tool: definition,
|
||||||
|
permission: { action: "question", resource: "*" },
|
||||||
|
authorize: ({ assertPermission }) =>
|
||||||
|
assertPermission({ action: "question", resources: ["*"] }).pipe(
|
||||||
|
Effect.mapError(() => new ToolFailure({ message: "Permission denied: question" })),
|
||||||
|
),
|
||||||
execute: ({ parameters, sessionID, source }) =>
|
execute: ({ parameters, sessionID, source }) =>
|
||||||
question
|
question
|
||||||
.ask({
|
.ask({
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type { SessionV2 } from "../session"
|
|||||||
import { ApplicationTools } from "./application-tools"
|
import { ApplicationTools } from "./application-tools"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
|
import { Wildcard } from "../util/wildcard"
|
||||||
|
|
||||||
export type ExecuteInput = {
|
export type ExecuteInput = {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
@@ -54,6 +55,8 @@ export type Entry<
|
|||||||
Success extends ToolSchema<any> = ToolSchema<any>,
|
Success extends ToolSchema<any> = ToolSchema<any>,
|
||||||
> = {
|
> = {
|
||||||
readonly tool: TypedTool<Parameters, Success>
|
readonly tool: TypedTool<Parameters, Success>
|
||||||
|
/** Catalog visibility only. Execution authorization remains leaf-owned. */
|
||||||
|
readonly permission?: { readonly action: string; readonly resource: "*" }
|
||||||
readonly authorize?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<void, ToolFailure>
|
readonly authorize?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<void, ToolFailure>
|
||||||
readonly execute?: (
|
readonly execute?: (
|
||||||
input: AuthorizeInput<Schema.Schema.Type<Parameters>>,
|
input: AuthorizeInput<Schema.Schema.Type<Parameters>>,
|
||||||
@@ -78,7 +81,9 @@ export type Editor = {
|
|||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
readonly contribute: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
readonly contribute: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
||||||
readonly definitions: () => Effect.Effect<ReadonlyArray<ReturnType<typeof Tool.toDefinitions>[number]>>
|
readonly definitions: (
|
||||||
|
permissions?: PermissionV2.Ruleset,
|
||||||
|
) => Effect.Effect<ReadonlyArray<ReturnType<typeof Tool.toDefinitions>[number]>>
|
||||||
readonly execute: (input: ExecuteInput) => Effect.Effect<ToolResultValue>
|
readonly execute: (input: ExecuteInput) => Effect.Effect<ToolResultValue>
|
||||||
readonly settle: (input: ExecuteInput) => Effect.Effect<Settlement>
|
readonly settle: (input: ExecuteInput) => Effect.Effect<Settlement>
|
||||||
}
|
}
|
||||||
@@ -114,13 +119,19 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
|
const definitions = Effect.fn("ToolRegistry.definitions")(function* (permissions: PermissionV2.Ruleset = []) {
|
||||||
const tools = new Map(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool] as const))
|
const tools = new Map(state.get().entries)
|
||||||
// Location tools own their names. Application tools fill otherwise-unclaimed names.
|
// Location tools own their names. Application tools fill otherwise-unclaimed names.
|
||||||
for (const [name, tool] of applications.entries()) {
|
for (const [name, tool] of applications.entries()) {
|
||||||
if (!tools.has(name)) tools.set(name, tool.definition)
|
if (!tools.has(name)) tools.set(name, { tool: tool.definition })
|
||||||
}
|
}
|
||||||
return Tool.toDefinitions(Object.fromEntries(tools))
|
return Tool.toDefinitions(
|
||||||
|
Object.fromEntries(
|
||||||
|
Array.from(tools)
|
||||||
|
.filter(([name, entry]) => !whollyDisabled(entry.permission ?? defaultPermission(name), permissions))
|
||||||
|
.map(([name, entry]) => [name, entry.tool]),
|
||||||
|
),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const entry = (name: string): Entry | undefined => {
|
const entry = (name: string): Entry | undefined => {
|
||||||
@@ -224,6 +235,15 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function defaultPermission(name: string) {
|
||||||
|
return { action: ["edit", "write", "apply_patch"].includes(name) ? "edit" : name, resource: "*" as const }
|
||||||
|
}
|
||||||
|
|
||||||
|
function whollyDisabled(permission: { readonly action: string; readonly resource: "*" }, rules: PermissionV2.Ruleset) {
|
||||||
|
const rule = rules.findLast((rule) => Wildcard.match(permission.action, rule.action))
|
||||||
|
return rule?.resource === "*" && rule.effect === "deny"
|
||||||
|
}
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(ApplicationTools.layer),
|
Layer.provide(ApplicationTools.layer),
|
||||||
Layer.provide(ToolOutputStore.defaultLayer),
|
Layer.provide(ToolOutputStore.defaultLayer),
|
||||||
|
|||||||
@@ -37,6 +37,26 @@ const contextual = (contexts: Tool.Context[]) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("ApplicationTools", () => {
|
describe("ApplicationTools", () => {
|
||||||
|
it.effect("filters an application tool by its name without adding execution authorization", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const contexts: Tool.Context[] = []
|
||||||
|
yield* applications.attach({ application_context: contextual(contexts) })
|
||||||
|
|
||||||
|
expect(yield* registry.definitions([{ action: "application_context", resource: "*", effect: "deny" }])).toEqual(
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-denied", name: "application_context", input: { query: "hello" } },
|
||||||
|
}),
|
||||||
|
).toMatchObject({ result: { type: "content" } })
|
||||||
|
expect(contexts).toEqual([{ sessionID, id: "call-denied", name: "application_context" }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("advertises and executes a scoped application tool with Session context", () =>
|
it.effect("advertises and executes a scoped application tool with Session context", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const applications = yield* ApplicationTools.Service
|
const applications = yield* ApplicationTools.Service
|
||||||
@@ -169,13 +189,18 @@ describe("ApplicationTools", () => {
|
|||||||
yield* transform((editor) =>
|
yield* transform((editor) =>
|
||||||
editor.set("shared", {
|
editor.set("shared", {
|
||||||
tool: location.definition,
|
tool: location.definition,
|
||||||
|
permission: { action: "question", resource: "*" },
|
||||||
execute: ({ parameters, sessionID, call }) =>
|
execute: ({ parameters, sessionID, call }) =>
|
||||||
location.execute(parameters, { sessionID, id: call.id, name: call.name }),
|
location.execute(parameters, { sessionID, id: call.id, name: call.name }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* applications.attach({ shared: contextual(applicationContexts) })
|
yield* applications.attach({ shared: contextual(applicationContexts) })
|
||||||
|
|
||||||
expect((yield* registry.definitions()).map((definition) => definition.name)).toEqual(["shared"])
|
expect(
|
||||||
|
(yield* registry.definitions([{ action: "question", resource: "*", effect: "deny" }])).map(
|
||||||
|
(definition) => definition.name,
|
||||||
|
),
|
||||||
|
).toEqual([])
|
||||||
expect(
|
expect(
|
||||||
yield* registry.settle({
|
yield* registry.settle({
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|||||||
@@ -449,6 +449,7 @@ describe("Config", () => {
|
|||||||
permission: {
|
permission: {
|
||||||
bash: "ask",
|
bash: "ask",
|
||||||
edit: { "*.md": "allow", "*": "deny" },
|
edit: { "*.md": "allow", "*": "deny" },
|
||||||
|
question: "deny",
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
reviewer: {
|
reviewer: {
|
||||||
@@ -526,6 +527,7 @@ describe("Config", () => {
|
|||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "edit", resource: "*.md", effect: "allow" },
|
{ action: "edit", resource: "*.md", effect: "allow" },
|
||||||
{ action: "edit", resource: "*", effect: "deny" },
|
{ action: "edit", resource: "*", effect: "deny" },
|
||||||
|
{ action: "question", resource: "*", effect: "deny" },
|
||||||
])
|
])
|
||||||
expect(documents[0]?.info.agents?.reviewer).toMatchObject({
|
expect(documents[0]?.info.agents?.reviewer).toMatchObject({
|
||||||
system: "Review changes.",
|
system: "Review changes.",
|
||||||
|
|||||||
@@ -45,6 +45,96 @@ const echo = Tool.make({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("ToolRegistry", () => {
|
describe("ToolRegistry", () => {
|
||||||
|
it.effect("matches V1 whole-tool filtering, edit aliases, and ordered wildcard precedence", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const transform = yield* registry.transform()
|
||||||
|
const sessionID = SessionV2.ID.make("ses_registry_filter")
|
||||||
|
yield* transform((editor) => {
|
||||||
|
editor.set("question", { tool: echo })
|
||||||
|
editor.set("bash", { tool: echo })
|
||||||
|
editor.set("edit", { tool: echo })
|
||||||
|
editor.set("write", { tool: echo })
|
||||||
|
editor.set("apply_patch", { tool: echo })
|
||||||
|
})
|
||||||
|
|
||||||
|
const names = (rules: PermissionV2.Ruleset) =>
|
||||||
|
registry.definitions(rules).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name)))
|
||||||
|
|
||||||
|
expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual([
|
||||||
|
"bash",
|
||||||
|
"edit",
|
||||||
|
"write",
|
||||||
|
"apply_patch",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(
|
||||||
|
yield* names([
|
||||||
|
{ action: "*", resource: "*", effect: "deny" },
|
||||||
|
{ action: "question", resource: "private", effect: "allow" },
|
||||||
|
]),
|
||||||
|
).toEqual(["question"])
|
||||||
|
|
||||||
|
expect(
|
||||||
|
yield* names([
|
||||||
|
{ action: "question", resource: "private", effect: "allow" },
|
||||||
|
{ action: "*", resource: "*", effect: "deny" },
|
||||||
|
]),
|
||||||
|
).toEqual([])
|
||||||
|
|
||||||
|
expect(yield* names([{ action: "question", resource: "*", effect: "ask" }])).toContain("question")
|
||||||
|
expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["question", "bash"])
|
||||||
|
expect(
|
||||||
|
yield* names([
|
||||||
|
{ action: "edit", resource: "*", effect: "deny" },
|
||||||
|
{ action: "edit", resource: "*.md", effect: "ask" },
|
||||||
|
]),
|
||||||
|
).toEqual(["question", "bash", "edit", "write", "apply_patch"])
|
||||||
|
expect(
|
||||||
|
yield* names([
|
||||||
|
{ action: "edit", resource: "*.md", effect: "allow" },
|
||||||
|
{ action: "edit", resource: "*", effect: "deny" },
|
||||||
|
]),
|
||||||
|
).toEqual(["question", "bash"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("settles only through concrete leaf authorization, not catalog visibility", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const transform = yield* registry.transform()
|
||||||
|
const sessionID = SessionV2.ID.make("ses_registry_stale")
|
||||||
|
let executed = false
|
||||||
|
yield* transform((editor) =>
|
||||||
|
editor.set("question", {
|
||||||
|
tool: echo,
|
||||||
|
permission: { action: "question", resource: "*" },
|
||||||
|
authorize: ({ assertPermission }) =>
|
||||||
|
assertPermission({ action: "question", resources: ["actual"] }).pipe(
|
||||||
|
Effect.mapError(() => new ToolFailure({ message: "Denied" })),
|
||||||
|
),
|
||||||
|
execute: () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
executed = true
|
||||||
|
return { text: "unexpected" }
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(yield* registry.definitions([{ action: "question", resource: "*", effect: "deny" }])).map((tool) => tool.name),
|
||||||
|
).toEqual([])
|
||||||
|
expect(
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-stale", name: "question", input: { text: "hello" } },
|
||||||
|
}),
|
||||||
|
).toMatchObject({ result: { type: "json", value: { text: "unexpected" } } })
|
||||||
|
expect(assertions.at(-1)).toMatchObject({ action: "question", resources: ["actual"] })
|
||||||
|
expect(executed).toBe(true)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("rebuilds advertised definitions when a scoped transform closes", () =>
|
it.effect("rebuilds advertised definitions when a scoped transform closes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ describe("BashTool", () => {
|
|||||||
const definitions = yield* registry.definitions()
|
const definitions = yield* registry.definitions()
|
||||||
expect(definitions.map((tool) => tool.name)).toEqual(["bash"])
|
expect(definitions.map((tool) => tool.name)).toEqual(["bash"])
|
||||||
expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.background")
|
expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.background")
|
||||||
|
expect(yield* registry.definitions([{ action: "bash", resource: "*", effect: "deny" }])).toEqual([])
|
||||||
expect(yield* registry.settle(call({ command: "pwd", description: "Print working directory" }))).toEqual({
|
expect(yield* registry.settle(call({ command: "pwd", description: "Print working directory" }))).toEqual({
|
||||||
result: { type: "text", value: "hello\n\n\nCommand exited with code 0." },
|
result: { type: "text", value: "hello\n\n\nCommand exited with code 0." },
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ describe("EditTool", () => {
|
|||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["edit"])
|
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["edit"])
|
||||||
|
expect(yield* registry.definitions([{ action: "edit", resource: "*", effect: "deny" }])).toEqual([])
|
||||||
const settled = yield* registry.settle(
|
const settled = yield* registry.settle(
|
||||||
call({ path: "hello.txt", oldString: "before", newString: "after" }),
|
call({ path: "hello.txt", oldString: "before", newString: "after" }),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ const sessionID = SessionV2.ID.make("ses_question_tool_test")
|
|||||||
const assertions: PermissionV2.AssertInput[] = []
|
const assertions: PermissionV2.AssertInput[] = []
|
||||||
let captured: QuestionV2.AskInput | undefined
|
let captured: QuestionV2.AskInput | undefined
|
||||||
let reject = false
|
let reject = false
|
||||||
|
let deny = false
|
||||||
const capturedInput = () => captured
|
const capturedInput = () => captured
|
||||||
const permission = Layer.succeed(
|
const permission = Layer.succeed(
|
||||||
PermissionV2.Service,
|
PermissionV2.Service,
|
||||||
PermissionV2.Service.of({
|
PermissionV2.Service.of({
|
||||||
assert: (input) => Effect.sync(() => assertions.push(input)),
|
assert: (input) =>
|
||||||
|
Effect.sync(() => assertions.push(input)).pipe(
|
||||||
|
Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
|
||||||
|
),
|
||||||
ask: () => Effect.die("unused"),
|
ask: () => Effect.die("unused"),
|
||||||
reply: () => Effect.die("unused"),
|
reply: () => Effect.die("unused"),
|
||||||
get: () => Effect.die("unused"),
|
get: () => Effect.die("unused"),
|
||||||
@@ -40,11 +44,30 @@ const tool = QuestionTool.layer.pipe(Layer.provide(registry), Layer.provide(ques
|
|||||||
const it = testEffect(Layer.mergeAll(permission, registry, question, tool))
|
const it = testEffect(Layer.mergeAll(permission, registry, question, tool))
|
||||||
|
|
||||||
describe("QuestionTool", () => {
|
describe("QuestionTool", () => {
|
||||||
|
it.effect("omits a denied built-in question and terminally settles a stale call", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
captured = undefined
|
||||||
|
deny = true
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
|
||||||
|
expect(yield* registry.definitions([{ action: "question", resource: "*", effect: "deny" }])).toEqual([])
|
||||||
|
expect(
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-question-denied", name: "question", input: { questions: [] } },
|
||||||
|
}),
|
||||||
|
).toEqual({ result: { type: "error", value: "Permission denied: question" } })
|
||||||
|
expect(capturedInput()).toBeUndefined()
|
||||||
|
deny = false
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("registers question and projects user answers without a permission assertion", () =>
|
it.effect("registers question and projects user answers without a permission assertion", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
assertions.length = 0
|
assertions.length = 0
|
||||||
captured = undefined
|
captured = undefined
|
||||||
reject = false
|
reject = false
|
||||||
|
deny = false
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
const questions = [
|
const questions = [
|
||||||
{
|
{
|
||||||
@@ -81,7 +104,7 @@ describe("QuestionTool", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(assertions).toEqual([])
|
expect(assertions).toEqual([{ sessionID, action: "question", resources: ["*"] }])
|
||||||
expect(capturedInput()).toEqual({ sessionID, questions, tool: undefined })
|
expect(capturedInput()).toEqual({ sessionID, questions, tool: undefined })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -90,6 +113,7 @@ describe("QuestionTool", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
captured = undefined
|
captured = undefined
|
||||||
reject = false
|
reject = false
|
||||||
|
deny = false
|
||||||
const registryService = yield* ToolRegistry.Service
|
const registryService = yield* ToolRegistry.Service
|
||||||
|
|
||||||
yield* registryService.execute({
|
yield* registryService.execute({
|
||||||
@@ -104,6 +128,7 @@ describe("QuestionTool", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
captured = undefined
|
captured = undefined
|
||||||
reject = true
|
reject = true
|
||||||
|
deny = false
|
||||||
const registryService = yield* ToolRegistry.Service
|
const registryService = yield* ToolRegistry.Service
|
||||||
const fiber = yield* registryService
|
const fiber = yield* registryService
|
||||||
.execute({
|
.execute({
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ describe("ReadTool", () => {
|
|||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
|
|
||||||
expect(yield* registry.definitions()).toMatchObject([{ name: "read" }])
|
expect(yield* registry.definitions()).toMatchObject([{ name: "read" }])
|
||||||
|
expect(yield* registry.definitions([{ action: "read", resource: "*", effect: "deny" }])).toEqual([])
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute({
|
yield* registry.execute({
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|||||||
Reference in New Issue
Block a user