fix(core): preserve denied permission context

This commit is contained in:
Kit Langton
2026-07-04 00:41:30 -04:00
parent fc20cd1e84
commit 77e1702ea2
10 changed files with 101 additions and 11 deletions
+9 -1
View File
@@ -74,7 +74,13 @@ export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("P
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
rules: Permission.Ruleset,
}) {}
permission: Schema.String,
resources: Schema.Array(Schema.String),
}) {
override get message() {
return `Permission denied: ${this.permission}`
}
}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
requestID: ID,
@@ -216,6 +222,8 @@ const layer = Layer.effect(
if (result.effect === "deny") {
return yield* new DeniedError({
rules: relevant(input, result.rules),
permission: input.action,
resources: input.resources,
})
}
if (result.effect === "allow") return
@@ -40,7 +40,7 @@ export function toSessionError(cause: unknown): SessionError.Error {
}
}
}
if (cause instanceof PermissionV2.RejectedError)
if (cause instanceof PermissionV2.DeniedError || cause instanceof PermissionV2.RejectedError)
return {
type: "permission.rejected",
message: cause.message,
+9 -1
View File
@@ -47,7 +47,15 @@ const permission = Layer.succeed(
}).pipe(
Effect.andThen(input.action === "edit" ? Effect.suspend(afterEditApproval) : Effect.void),
Effect.andThen(
input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
input.action === denyAction
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
+9 -1
View File
@@ -40,7 +40,15 @@ const permission = Layer.succeed(
assert: (input) =>
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(
input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
input.action === denyAction
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
+20 -2
View File
@@ -23,7 +23,17 @@ const permission = Layer.succeed(
PermissionV2.Service.of({
assert: (input) =>
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
Effect.andThen(
deny
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
@@ -72,7 +82,15 @@ describe("QuestionTool", () => {
...toolIdentity,
call: { type: "tool-call", id: "call-question-denied", name: "question", input: { questions: [] } },
}),
).toEqual({ result: { type: "error", value: "Permission denied: question" } })
).toEqual({
result: { type: "error", value: "Permission denied: question" },
error: {
type: "permission.rejected",
message: "Permission denied: question",
permission: "question",
resources: ["*"],
},
})
expect(capturedInput()).toBeUndefined()
deny = false
}),
+13 -1
View File
@@ -81,7 +81,19 @@ const permission = Layer.succeed(
assert: (input) =>
Effect.sync(() => {
assertions.push(input)
}).pipe(Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] })))),
}).pipe(
Effect.andThen(
allow
? Effect.void
: Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
),
),
),
ask: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
get: () => Effect.die("unused"),
+9 -1
View File
@@ -47,7 +47,15 @@ const permission = Layer.succeed(
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(Effect.suspend(() => afterPermission(input))),
Effect.andThen(
input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
input.action === denyAction
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
+11 -1
View File
@@ -55,7 +55,17 @@ describe("SkillTool", () => {
PermissionV2.Service.of({
assert: (input) =>
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
Effect.andThen(
deny
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
+11 -1
View File
@@ -33,7 +33,17 @@ const permission = Layer.succeed(
PermissionV2.Service.of({
assert: (input) =>
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
Effect.andThen(
deny
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
+9 -1
View File
@@ -38,7 +38,15 @@ const permission = Layer.succeed(
assert: (input) =>
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(
input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
input.action === denyAction
? Effect.fail(
new PermissionV2.DeniedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),