feat(core): add location-based permission service (#30287)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { PermissionNotFoundError } from "../errors"
|
||||
@@ -10,7 +10,7 @@ import { described } from "./metadata"
|
||||
|
||||
const root = "/permission"
|
||||
const ReplyPayload = Schema.Struct({
|
||||
reply: Permission.Reply,
|
||||
reply: PermissionLegacy.Reply,
|
||||
message: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Permission.Request), "List of pending permissions"),
|
||||
success: described(Schema.Array(PermissionLegacy.Request), "List of pending permissions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.list",
|
||||
@@ -29,7 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||
params: { requestID: PermissionID },
|
||||
params: { requestID: PermissionLegacy.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ReplyPayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
@@ -48,7 +48,7 @@ export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
|
||||
export const UpdatePayload = Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Session.Metadata),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
permission: Schema.optional(PermissionLegacy.Ruleset),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
archived: Schema.optional(Session.ArchivedTimestamp),
|
||||
@@ -71,7 +71,7 @@ export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInp
|
||||
export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"]))
|
||||
export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"]))
|
||||
export const PermissionResponsePayload = Schema.Struct({
|
||||
response: Permission.Reply,
|
||||
response: PermissionLegacy.Reply,
|
||||
})
|
||||
|
||||
export const SessionPaths = {
|
||||
@@ -392,7 +392,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||
params: { sessionID: SessionID, permissionID: PermissionLegacy.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PermissionResponsePayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
|
||||
@@ -3,12 +3,16 @@ import { MessageGroup } from "./v2/message"
|
||||
import { ModelGroup } from "./v2/model"
|
||||
import { ProviderGroup } from "./v2/provider"
|
||||
import { SessionGroup } from "./v2/session"
|
||||
import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./v2/permission"
|
||||
|
||||
export const V2Api = HttpApi.make("v2")
|
||||
.add(SessionGroup)
|
||||
.add(MessageGroup)
|
||||
.add(ModelGroup)
|
||||
.add(ProviderGroup)
|
||||
.add(PermissionGroup)
|
||||
.add(SessionPermissionGroup)
|
||||
.add(PermissionSavedGroup)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
@@ -34,7 +35,7 @@ export const locationQueryOpenApi = OpenApi.annotations({
|
||||
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
V2LocationMiddleware,
|
||||
{
|
||||
provides: Catalog.Service | PluginBoot.Service
|
||||
provides: Catalog.Service | PluginBoot.Service | PermissionV2.Service
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiV2Location") {}
|
||||
|
||||
@@ -59,4 +60,4 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
}),
|
||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("permissionRequests", "/api/permission/request", {
|
||||
query: LocationQuery,
|
||||
success: Schema.Array(PermissionV2.Request),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.request.list",
|
||||
summary: "List pending permission requests",
|
||||
description: "Retrieve pending permission requests for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 permissions", description: "Experimental v2 permission routes." }))
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Array(PermissionV2.Request),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.list",
|
||||
summary: "List session permission requests",
|
||||
description: "Retrieve pending permission requests owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("permissionRequestReply", "/api/session/:sessionID/permission/request/:requestID/reply", {
|
||||
params: { sessionID: SessionV2.ID, requestID: PermissionV2.ID },
|
||||
payload: Schema.Struct({
|
||||
reply: PermissionV2.Reply,
|
||||
message: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, PermissionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.reply",
|
||||
summary: "Reply to pending permission request",
|
||||
description: "Respond to a pending permission request owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 session permissions", description: "Experimental v2 session permission routes." }))
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
||||
.add(
|
||||
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
|
||||
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Array(PermissionSaved.Info),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.list",
|
||||
summary: "List saved permissions",
|
||||
description: "Retrieve saved permissions, optionally filtered by project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("removeSavedPermission", "/api/permission/saved/:id", {
|
||||
params: { id: PermissionSaved.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.remove",
|
||||
summary: "Remove saved permission",
|
||||
description: "Remove a saved permission by ID.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 saved permissions", description: "Experimental v2 saved permission routes." }))
|
||||
.middleware(V2Authorization)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
@@ -14,8 +14,8 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
|
||||
})
|
||||
|
||||
const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
|
||||
params: { requestID: PermissionID }
|
||||
payload: Permission.ReplyBody
|
||||
params: { requestID: PermissionLegacy.ID }
|
||||
payload: PermissionLegacy.ReplyBody
|
||||
}) {
|
||||
yield* svc
|
||||
.reply({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Command } from "@/command"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionCompaction } from "@/session/compaction"
|
||||
@@ -360,7 +360,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
})
|
||||
|
||||
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
||||
params: { sessionID: SessionID; permissionID: PermissionID }
|
||||
params: { sessionID: SessionID; permissionID: PermissionLegacy.ID }
|
||||
payload: typeof PermissionResponsePayload.Type
|
||||
}) {
|
||||
yield* requireSession(ctx.params.sessionID)
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Layer } from "effect"
|
||||
import { layer as v2LocationLayer } from "../groups/v2/location"
|
||||
import { messageHandlers } from "./v2/message"
|
||||
import { modelHandlers } from "./v2/model"
|
||||
import { providerHandlers } from "./v2/provider"
|
||||
import { sessionHandlers } from "./v2/session"
|
||||
import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./v2/permission"
|
||||
|
||||
export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers, modelHandlers, providerHandlers).pipe(
|
||||
export const v2Handlers = Layer.mergeAll(
|
||||
sessionHandlers,
|
||||
messageHandlers,
|
||||
modelHandlers,
|
||||
providerHandlers,
|
||||
permissionHandlers,
|
||||
sessionPermissionHandlers,
|
||||
savedPermissionHandlers,
|
||||
).pipe(
|
||||
Layer.provide(v2LocationLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(PermissionSaved.layer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"permissionRequests",
|
||||
Effect.fn(function* () {
|
||||
return yield* (yield* PermissionV2.Service).list()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const withSessionPermission = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: Parameters<PermissionV2.Interface["forSession"]>[0],
|
||||
use: (permission: PermissionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
return yield* use(yield* PermissionV2.Service)
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(
|
||||
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"sessionPermissionRequests",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
permission.forSession(ctx.params.sessionID),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"permissionRequestReply",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* permission.get(ctx.params.requestID)
|
||||
if (!request || request.sessionID !== ctx.params.sessionID)
|
||||
return yield* missingRequest(ctx.params.requestID)
|
||||
yield* permission
|
||||
.reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
|
||||
.pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
|
||||
}),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const savedPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission.saved", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const saved = yield* PermissionSaved.Service
|
||||
return handlers
|
||||
.handle(
|
||||
"savedPermissions",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* saved.list({ projectID: ctx.query.projectID })
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"removeSavedPermission",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* saved.remove(ctx.params.id)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
Reference in New Issue
Block a user