feat(core): add location-based permission service (#30287)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
export * as PermissionLegacy from "./legacy"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { withStatics } from "../schema"
|
||||
import { SessionSchema } from "../session/schema"
|
||||
import { Identifier } from "../util/identifier"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
|
||||
Schema.brand("PermissionID"),
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" })
|
||||
export type Action = typeof Action.Type
|
||||
|
||||
export const Rule = Schema.Struct({
|
||||
permission: Schema.String,
|
||||
pattern: Schema.String,
|
||||
action: Action,
|
||||
}).annotate({ identifier: "PermissionRule" })
|
||||
export type Rule = typeof Rule.Type
|
||||
|
||||
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
|
||||
export type Ruleset = typeof Ruleset.Type
|
||||
|
||||
export const Request = Schema.Struct({
|
||||
id: ID,
|
||||
sessionID: SessionSchema.ID,
|
||||
permission: Schema.String,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||
always: Schema.Array(Schema.String),
|
||||
tool: Schema.Struct({
|
||||
messageID: Schema.String,
|
||||
callID: Schema.String,
|
||||
}).pipe(Schema.optional),
|
||||
}).annotate({ identifier: "PermissionRequest" })
|
||||
export type Request = typeof Request.Type
|
||||
|
||||
export const Reply = Schema.Literals(["once", "always", "reject"])
|
||||
export type Reply = typeof Reply.Type
|
||||
|
||||
export const ReplyBody = Schema.Struct({
|
||||
reply: Reply,
|
||||
message: Schema.String.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "PermissionReplyBody" })
|
||||
export type ReplyBody = typeof ReplyBody.Type
|
||||
|
||||
export const Approval = Schema.Struct({
|
||||
projectID: ProjectV2.ID,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PermissionApproval" })
|
||||
export type Approval = typeof Approval.Type
|
||||
|
||||
export const AskInput = Schema.Struct({
|
||||
...Request.fields,
|
||||
id: ID.pipe(Schema.optional),
|
||||
ruleset: Ruleset,
|
||||
}).annotate({ identifier: "PermissionAskInput" })
|
||||
export type AskInput = typeof AskInput.Type
|
||||
|
||||
export const ReplyInput = Schema.Struct({
|
||||
requestID: ID,
|
||||
...ReplyBody.fields,
|
||||
}).annotate({ identifier: "PermissionReplyInput" })
|
||||
export type ReplyInput = typeof ReplyInput.Type
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) {
|
||||
override get message() {
|
||||
return "The user rejected permission to use this specific tool call."
|
||||
}
|
||||
}
|
||||
|
||||
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionCorrectedError", {
|
||||
feedback: Schema.String,
|
||||
}) {
|
||||
override get message() {
|
||||
return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionDeniedError", {
|
||||
ruleset: Schema.Any,
|
||||
}) {
|
||||
override get message() {
|
||||
return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}`
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
|
||||
requestID: ID,
|
||||
}) {}
|
||||
|
||||
export type Error = DeniedError | RejectedError | CorrectedError
|
||||
@@ -0,0 +1,78 @@
|
||||
export * as PermissionSaved from "./saved"
|
||||
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { withStatics } from "../schema"
|
||||
import { Identifier } from "../util/identifier"
|
||||
import { PermissionTable } from "./sql"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("PermissionSaved.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("psv_" + Identifier.ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
projectID: ProjectV2.ID,
|
||||
action: Schema.String,
|
||||
resource: Schema.String,
|
||||
}).annotate({ identifier: "PermissionSaved.Info" })
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
export const ListInput = Schema.Struct({
|
||||
projectID: ProjectV2.ID.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "PermissionSaved.ListInput" })
|
||||
export type ListInput = typeof ListInput.Type
|
||||
|
||||
export const AddInput = Schema.Struct({
|
||||
projectID: ProjectV2.ID,
|
||||
action: Schema.String,
|
||||
resources: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PermissionSaved.AddInput" })
|
||||
export type AddInput = typeof AddInput.Type
|
||||
|
||||
export interface Interface {
|
||||
readonly list: (input?: ListInput) => Effect.Effect<ReadonlyArray<Info>>
|
||||
readonly add: (input: AddInput) => Effect.Effect<void>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PermissionSaved") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const list = Effect.fn("PermissionSaved.list")(function* (input?: ListInput) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(PermissionTable)
|
||||
.where(input?.projectID ? eq(PermissionTable.project_id, input.projectID) : undefined)
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.map((row): Info => ({ id: row.id, projectID: row.project_id, action: row.action, resource: row.resource }))
|
||||
})
|
||||
|
||||
const add = Effect.fn("PermissionSaved.add")(function* (input: AddInput) {
|
||||
if (!input.resources.length) return
|
||||
yield* db
|
||||
.insert(PermissionTable)
|
||||
.values(input.resources.map((resource) => ({ id: ID.create(), project_id: input.projectID, action: input.action, resource })))
|
||||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const remove = Effect.fn("PermissionSaved.remove")(function* (id: ID) {
|
||||
yield* db.delete(PermissionTable).where(eq(PermissionTable.id, id)).run().pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
return Service.of({ list, add, remove })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||
@@ -0,0 +1,16 @@
|
||||
export * as PermissionSchema from "./schema"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" })
|
||||
export type Effect = typeof Effect.Type
|
||||
|
||||
export const Rule = Schema.Struct({
|
||||
action: Schema.String,
|
||||
resource: Schema.String,
|
||||
effect: Effect,
|
||||
}).annotate({ identifier: "PermissionV2.Rule" })
|
||||
export type Rule = typeof Rule.Type
|
||||
|
||||
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" })
|
||||
export type Ruleset = typeof Ruleset.Type
|
||||
@@ -0,0 +1,20 @@
|
||||
import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { ProjectTable } from "../project/sql"
|
||||
import type { PermissionSaved } from "./saved"
|
||||
|
||||
export const PermissionTable = sqliteTable(
|
||||
"permission",
|
||||
{
|
||||
id: text().$type<PermissionSaved.ID>().primaryKey(),
|
||||
project_id: text()
|
||||
.$type<ProjectV2.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
action: text().notNull(),
|
||||
resource: text().notNull(),
|
||||
...Timestamps,
|
||||
},
|
||||
(table) => [uniqueIndex("permission_project_action_resource_idx").on(table.project_id, table.action, table.resource)],
|
||||
)
|
||||
Reference in New Issue
Block a user