test(core): cover permission denial continuation
This commit is contained in:
parent
fa2b63f850
commit
8bb211ebfd
1 changed files with 61 additions and 3 deletions
|
|
@ -5,6 +5,7 @@ import {
|
||||||
LLMEvent,
|
LLMEvent,
|
||||||
Model,
|
Model,
|
||||||
Tool,
|
Tool,
|
||||||
|
ToolFailure,
|
||||||
TransportReason,
|
TransportReason,
|
||||||
InvalidRequestReason,
|
InvalidRequestReason,
|
||||||
type LLMClientShape,
|
type LLMClientShape,
|
||||||
|
|
@ -112,6 +113,7 @@ const recoveryModel = Model.make({
|
||||||
})
|
})
|
||||||
const authorizations: ToolRegistry.AuthorizeInput[] = []
|
const authorizations: ToolRegistry.AuthorizeInput[] = []
|
||||||
const executions: string[] = []
|
const executions: string[] = []
|
||||||
|
let denyEcho = false
|
||||||
const permission = Layer.succeed(
|
const permission = Layer.succeed(
|
||||||
PermissionV2.Service,
|
PermissionV2.Service,
|
||||||
PermissionV2.Service.of({
|
PermissionV2.Service.of({
|
||||||
|
|
@ -135,9 +137,18 @@ const echo = Layer.effectDiscard(
|
||||||
registry.contribute((editor) => {
|
registry.contribute((editor) => {
|
||||||
;(editor.set("echo", {
|
;(editor.set("echo", {
|
||||||
authorize: (input) =>
|
authorize: (input) =>
|
||||||
Effect.sync(() => {
|
denyEcho
|
||||||
authorizations.push(input)
|
? Effect.fail(
|
||||||
}),
|
new ToolFailure({
|
||||||
|
message: "Permission denied",
|
||||||
|
error: new PermissionV2.DeniedError({
|
||||||
|
rules: [{ action: "echo", resource: "*", effect: "deny" }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: Effect.sync(() => {
|
||||||
|
authorizations.push(input)
|
||||||
|
}),
|
||||||
tool: Tool.make({
|
tool: Tool.make({
|
||||||
description: "Echo text",
|
description: "Echo text",
|
||||||
parameters: Schema.Struct({ text: Schema.String }),
|
parameters: Schema.Struct({ text: Schema.String }),
|
||||||
|
|
@ -1809,6 +1820,53 @@ describe("SessionRunnerLLM", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("continues after a permission is denied without awaiting a responder", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Try the denied tool" }), resume: false })
|
||||||
|
|
||||||
|
requests.length = 0
|
||||||
|
executions.length = 0
|
||||||
|
denyEcho = true
|
||||||
|
responses = [
|
||||||
|
[
|
||||||
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
|
LLMEvent.toolCall({ id: "call-denied", name: "echo", input: { text: "blocked" } }),
|
||||||
|
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||||
|
LLMEvent.finish({ reason: "tool-calls" }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
|
LLMEvent.textStart({ id: "text-after-denial" }),
|
||||||
|
LLMEvent.textDelta({ id: "text-after-denial", text: "Permission denied" }),
|
||||||
|
LLMEvent.textEnd({ id: "text-after-denial" }),
|
||||||
|
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||||
|
LLMEvent.finish({ reason: "stop" }),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
yield* session.resume(sessionID).pipe(Effect.ensuring(Effect.sync(() => (denyEcho = false))))
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(2)
|
||||||
|
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "tool"])
|
||||||
|
expect(executions).toEqual([])
|
||||||
|
expect(yield* session.context(sessionID)).toMatchObject([
|
||||||
|
{ type: "user", text: "Try the denied tool" },
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
finish: "tool-calls",
|
||||||
|
content: [{ type: "tool", id: "call-denied", name: "echo", state: { status: "error" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
finish: "stop",
|
||||||
|
content: [{ type: "text", id: "text-after-denial", text: "Permission denied" }],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("reloads a model switch before a tool-driven continuation turn", () =>
|
it.effect("reloads a model switch before a tool-driven continuation turn", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue