add experimental permission HttpApi slice (#22385)
This commit is contained in:
@@ -7,75 +7,84 @@ import { Instance } from "@/project/instance"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
import { PermissionTable } from "@/session/session.sql"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { Log } from "@/util/log"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
import { evaluate as evalRule } from "./evaluate"
|
||||
import { PermissionID } from "./schema"
|
||||
|
||||
export namespace Permission {
|
||||
const log = Log.create({ service: "permission" })
|
||||
|
||||
export const Action = z.enum(["allow", "deny", "ask"]).meta({
|
||||
ref: "PermissionAction",
|
||||
})
|
||||
export type Action = z.infer<typeof Action>
|
||||
export const Action = Schema.Literals(["allow", "deny", "ask"])
|
||||
.annotate({ identifier: "PermissionAction" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Action = Schema.Schema.Type<typeof Action>
|
||||
|
||||
export const Rule = z
|
||||
.object({
|
||||
permission: z.string(),
|
||||
pattern: z.string(),
|
||||
action: Action,
|
||||
})
|
||||
.meta({
|
||||
ref: "PermissionRule",
|
||||
})
|
||||
export type Rule = z.infer<typeof Rule>
|
||||
export class Rule extends Schema.Class<Rule>("PermissionRule")({
|
||||
permission: Schema.String,
|
||||
pattern: Schema.String,
|
||||
action: Action,
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
|
||||
export const Ruleset = Rule.array().meta({
|
||||
ref: "PermissionRuleset",
|
||||
})
|
||||
export type Ruleset = z.infer<typeof Ruleset>
|
||||
export const Ruleset = Schema.mutable(Schema.Array(Rule))
|
||||
.annotate({ identifier: "PermissionRuleset" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Ruleset = Schema.Schema.Type<typeof Ruleset>
|
||||
|
||||
export const Request = z
|
||||
.object({
|
||||
id: PermissionID.zod,
|
||||
sessionID: SessionID.zod,
|
||||
permission: z.string(),
|
||||
patterns: z.string().array(),
|
||||
metadata: z.record(z.string(), z.any()),
|
||||
always: z.string().array(),
|
||||
tool: z
|
||||
.object({
|
||||
messageID: MessageID.zod,
|
||||
callID: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "PermissionRequest",
|
||||
})
|
||||
export type Request = z.infer<typeof Request>
|
||||
export class Request extends Schema.Class<Request>("PermissionRequest")({
|
||||
id: PermissionID,
|
||||
sessionID: SessionID,
|
||||
permission: Schema.String,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||
always: Schema.Array(Schema.String),
|
||||
tool: Schema.optional(
|
||||
Schema.Struct({
|
||||
messageID: MessageID,
|
||||
callID: Schema.String,
|
||||
}),
|
||||
),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
|
||||
export const Reply = z.enum(["once", "always", "reject"])
|
||||
export type Reply = z.infer<typeof Reply>
|
||||
export const Reply = Schema.Literals(["once", "always", "reject"]).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Reply = Schema.Schema.Type<typeof Reply>
|
||||
|
||||
export const Approval = z.object({
|
||||
projectID: ProjectID.zod,
|
||||
patterns: z.string().array(),
|
||||
})
|
||||
const reply = {
|
||||
reply: Reply,
|
||||
message: Schema.optional(Schema.String),
|
||||
}
|
||||
|
||||
export const ReplyBody = Schema.Struct(reply)
|
||||
.annotate({ identifier: "PermissionReplyBody" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ReplyBody = Schema.Schema.Type<typeof ReplyBody>
|
||||
|
||||
export class Approval extends Schema.Class<Approval>("PermissionApproval")({
|
||||
projectID: ProjectID,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Asked: BusEvent.define("permission.asked", Request),
|
||||
Asked: BusEvent.define("permission.asked", Request.zod),
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
requestID: PermissionID.zod,
|
||||
reply: Reply,
|
||||
}),
|
||||
zod(
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: PermissionID,
|
||||
reply: Reply,
|
||||
}),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -103,20 +112,27 @@ export namespace Permission {
|
||||
|
||||
export type Error = DeniedError | RejectedError | CorrectedError
|
||||
|
||||
export const AskInput = Request.partial({ id: true }).extend({
|
||||
export const AskInput = Schema.Struct({
|
||||
...Request.fields,
|
||||
id: Schema.optional(PermissionID),
|
||||
ruleset: Ruleset,
|
||||
})
|
||||
.annotate({ identifier: "PermissionAskInput" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type AskInput = Schema.Schema.Type<typeof AskInput>
|
||||
|
||||
export const ReplyInput = z.object({
|
||||
requestID: PermissionID.zod,
|
||||
reply: Reply,
|
||||
message: z.string().optional(),
|
||||
export const ReplyInput = Schema.Struct({
|
||||
requestID: PermissionID,
|
||||
...reply,
|
||||
})
|
||||
.annotate({ identifier: "PermissionReplyInput" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ReplyInput = Schema.Schema.Type<typeof ReplyInput>
|
||||
|
||||
export interface Interface {
|
||||
readonly ask: (input: z.infer<typeof AskInput>) => Effect.Effect<void, Error>
|
||||
readonly reply: (input: z.infer<typeof ReplyInput>) => Effect.Effect<void>
|
||||
readonly list: () => Effect.Effect<Request[]>
|
||||
readonly ask: (input: AskInput) => Effect.Effect<void, Error>
|
||||
readonly reply: (input: ReplyInput) => Effect.Effect<void>
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||
}
|
||||
|
||||
interface PendingEntry {
|
||||
@@ -163,7 +179,7 @@ export namespace Permission {
|
||||
}),
|
||||
)
|
||||
|
||||
const ask = Effect.fn("Permission.ask")(function* (input: z.infer<typeof AskInput>) {
|
||||
const ask = Effect.fn("Permission.ask")(function* (input: AskInput) {
|
||||
const { approved, pending } = yield* InstanceState.get(state)
|
||||
const { ruleset, ...request } = input
|
||||
let needsAsk = false
|
||||
@@ -183,10 +199,10 @@ export namespace Permission {
|
||||
if (!needsAsk) return
|
||||
|
||||
const id = request.id ?? PermissionID.ascending()
|
||||
const info: Request = {
|
||||
const info = Schema.decodeUnknownSync(Request)({
|
||||
id,
|
||||
...request,
|
||||
}
|
||||
})
|
||||
log.info("asking", { id, permission: info.permission, patterns: info.patterns })
|
||||
|
||||
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
||||
@@ -200,7 +216,7 @@ export namespace Permission {
|
||||
)
|
||||
})
|
||||
|
||||
const reply = Effect.fn("Permission.reply")(function* (input: z.infer<typeof ReplyInput>) {
|
||||
const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) {
|
||||
const { approved, pending } = yield* InstanceState.get(state)
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) return
|
||||
|
||||
@@ -2,9 +2,13 @@ import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { ZodOverride } from "@/util/effect-zod"
|
||||
import { Newtype } from "@/util/schema"
|
||||
|
||||
export class PermissionID extends Newtype<PermissionID>()("PermissionID", Schema.String) {
|
||||
export class PermissionID extends Newtype<PermissionID>()(
|
||||
"PermissionID",
|
||||
Schema.String.annotate({ [ZodOverride]: Identifier.schema("permission") }),
|
||||
) {
|
||||
static ascending(id?: string): PermissionID {
|
||||
return this.make(Identifier.ascending("permission", id))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user