feat(server): add permission request endpoints (#33774)
This commit is contained in:
@@ -814,6 +814,20 @@ const scenarios: Scenario[] = [
|
|||||||
object(body.location)
|
object(body.location)
|
||||||
array(body.data)
|
array(body.data)
|
||||||
}),
|
}),
|
||||||
|
http.protected
|
||||||
|
.post("/api/session/{sessionID}/permission", "v2.session.permission.create")
|
||||||
|
.seeded((ctx) => ctx.session({ title: "Permission create owner" }))
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: route("/api/session/{sessionID}/permission", { sessionID: ctx.state.id }),
|
||||||
|
headers: ctx.headers(),
|
||||||
|
body: { action: "read", resources: [".env"] },
|
||||||
|
}))
|
||||||
|
.json(200, (body) => {
|
||||||
|
object(body)
|
||||||
|
object(body.data)
|
||||||
|
check(typeof body.data.id === "string", "permission create should return an ID")
|
||||||
|
check(body.data.effect === "ask", "permission create should create a pending request")
|
||||||
|
}),
|
||||||
http.protected
|
http.protected
|
||||||
.get("/api/session/{sessionID}/permission", "v2.session.permission.list")
|
.get("/api/session/{sessionID}/permission", "v2.session.permission.list")
|
||||||
.seeded((ctx) => ctx.session({ title: "Permission list owner" }))
|
.seeded((ctx) => ctx.session({ title: "Permission list owner" }))
|
||||||
@@ -822,6 +836,17 @@ const scenarios: Scenario[] = [
|
|||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
}))
|
}))
|
||||||
.json(200, data(array)),
|
.json(200, data(array)),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session/{sessionID}/permission/{requestID}", "v2.session.permission.get")
|
||||||
|
.seeded((ctx) => ctx.session({ title: "Permission get owner" }))
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: route("/api/session/{sessionID}/permission/{requestID}", {
|
||||||
|
sessionID: ctx.state.id,
|
||||||
|
requestID: "per_httpapi_missing",
|
||||||
|
}),
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.json(404, object, "status"),
|
||||||
http.protected
|
http.protected
|
||||||
.get("/api/session/{sessionID}/question", "v2.session.question.list")
|
.get("/api/session/{sessionID}/question", "v2.session.question.list")
|
||||||
.seeded((ctx) => ctx.session({ title: "Question list owner" }))
|
.seeded((ctx) => ctx.session({ title: "Question list owner" }))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Permission } from "@opencode-ai/schema/permission"
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
import { Location } from "@opencode-ai/schema/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import { Permission } from "@opencode-ai/schema/permission"
|
||||||
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||||
import { Project } from "@opencode-ai/schema/project"
|
import { Project } from "@opencode-ai/schema/project"
|
||||||
import { Session } from "@opencode-ai/schema/session"
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
@@ -58,6 +59,32 @@ export const makePermissionGroup = <
|
|||||||
)
|
)
|
||||||
// Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
|
// Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
|
||||||
.middleware(locationMiddleware)
|
.middleware(locationMiddleware)
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.post("session.permission.create", "/api/session/:sessionID/permission", {
|
||||||
|
params: { sessionID: Session.ID },
|
||||||
|
payload: Schema.Struct({
|
||||||
|
id: Permission.ID.pipe(Schema.optional),
|
||||||
|
action: Permission.Request.fields.action,
|
||||||
|
resources: Permission.Request.fields.resources,
|
||||||
|
save: Permission.Request.fields.save,
|
||||||
|
metadata: Permission.Request.fields.metadata,
|
||||||
|
source: Permission.Request.fields.source,
|
||||||
|
agent: Agent.ID.pipe(Schema.optional),
|
||||||
|
}),
|
||||||
|
success: Schema.Struct({
|
||||||
|
data: Schema.Struct({ id: Permission.ID, effect: Permission.Effect }),
|
||||||
|
}),
|
||||||
|
error: SessionNotFoundError,
|
||||||
|
})
|
||||||
|
.middleware(sessionLocationMiddleware)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "v2.session.permission.create",
|
||||||
|
summary: "Create permission request",
|
||||||
|
description: "Evaluate and, when approval is required, create a permission request for a session.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
|
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
|
||||||
params: { sessionID: Session.ID },
|
params: { sessionID: Session.ID },
|
||||||
@@ -73,6 +100,21 @@ export const makePermissionGroup = <
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("session.permission.get", "/api/session/:sessionID/permission/:requestID", {
|
||||||
|
params: { sessionID: Session.ID, requestID: Permission.ID },
|
||||||
|
success: Schema.Struct({ data: Permission.Request }),
|
||||||
|
error: [SessionNotFoundError, PermissionNotFoundError],
|
||||||
|
})
|
||||||
|
.middleware(sessionLocationMiddleware)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "v2.session.permission.get",
|
||||||
|
summary: "Get permission request",
|
||||||
|
description: "Retrieve a pending permission request owned by a session.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("session.permission.reply", "/api/session/:sessionID/permission/:requestID/reply", {
|
HttpApiEndpoint.post("session.permission.reply", "/api/session/:sessionID/permission/:requestID/reply", {
|
||||||
params: { sessionID: Session.ID, requestID: Permission.ID },
|
params: { sessionID: Session.ID, requestID: Permission.ID },
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ import type {
|
|||||||
PermissionRespondResponses,
|
PermissionRespondResponses,
|
||||||
PermissionRuleset,
|
PermissionRuleset,
|
||||||
PermissionV2Reply,
|
PermissionV2Reply,
|
||||||
|
PermissionV2Source,
|
||||||
ProjectCurrentErrors,
|
ProjectCurrentErrors,
|
||||||
ProjectCurrentResponses,
|
ProjectCurrentResponses,
|
||||||
ProjectDirectoriesErrors,
|
ProjectDirectoriesErrors,
|
||||||
@@ -341,6 +342,10 @@ import type {
|
|||||||
V2SessionListResponses,
|
V2SessionListResponses,
|
||||||
V2SessionMessagesErrors,
|
V2SessionMessagesErrors,
|
||||||
V2SessionMessagesResponses,
|
V2SessionMessagesResponses,
|
||||||
|
V2SessionPermissionCreateErrors,
|
||||||
|
V2SessionPermissionCreateResponses,
|
||||||
|
V2SessionPermissionGetErrors,
|
||||||
|
V2SessionPermissionGetResponses,
|
||||||
V2SessionPermissionListErrors,
|
V2SessionPermissionListErrors,
|
||||||
V2SessionPermissionListResponses,
|
V2SessionPermissionListResponses,
|
||||||
V2SessionPermissionReplyErrors,
|
V2SessionPermissionReplyErrors,
|
||||||
@@ -5183,6 +5188,93 @@ export class Permission2 extends HeyApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create permission request
|
||||||
|
*
|
||||||
|
* Evaluate and, when approval is required, create a permission request for a session.
|
||||||
|
*/
|
||||||
|
public create<ThrowOnError extends boolean = false>(
|
||||||
|
parameters: {
|
||||||
|
sessionID: string
|
||||||
|
id?: string
|
||||||
|
action?: string
|
||||||
|
resources?: Array<string>
|
||||||
|
save?: Array<string>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
source?: PermissionV2Source
|
||||||
|
agent?: string
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "path", key: "sessionID" },
|
||||||
|
{ in: "body", key: "id" },
|
||||||
|
{ in: "body", key: "action" },
|
||||||
|
{ in: "body", key: "resources" },
|
||||||
|
{ in: "body", key: "save" },
|
||||||
|
{ in: "body", key: "metadata" },
|
||||||
|
{ in: "body", key: "source" },
|
||||||
|
{ in: "body", key: "agent" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).post<
|
||||||
|
V2SessionPermissionCreateResponses,
|
||||||
|
V2SessionPermissionCreateErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
url: "/api/session/{sessionID}/permission",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options?.headers,
|
||||||
|
...params.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get permission request
|
||||||
|
*
|
||||||
|
* Retrieve a pending permission request owned by a session.
|
||||||
|
*/
|
||||||
|
public get<ThrowOnError extends boolean = false>(
|
||||||
|
parameters: {
|
||||||
|
sessionID: string
|
||||||
|
requestID: string
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "path", key: "sessionID" },
|
||||||
|
{ in: "path", key: "requestID" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).get<
|
||||||
|
V2SessionPermissionGetResponses,
|
||||||
|
V2SessionPermissionGetErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
url: "/api/session/{sessionID}/permission/{requestID}",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reply to pending permission request
|
* Reply to pending permission request
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -12581,6 +12581,95 @@ export type V2SessionPermissionListResponses = {
|
|||||||
|
|
||||||
export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses]
|
export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses]
|
||||||
|
|
||||||
|
export type V2SessionPermissionCreateData = {
|
||||||
|
body: {
|
||||||
|
id?: string
|
||||||
|
action: string
|
||||||
|
resources: Array<string>
|
||||||
|
save?: Array<string>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
source?: PermissionV2Source
|
||||||
|
agent?: string
|
||||||
|
}
|
||||||
|
path: {
|
||||||
|
sessionID: string
|
||||||
|
}
|
||||||
|
query?: never
|
||||||
|
url: "/api/session/{sessionID}/permission"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2SessionPermissionCreateErrors = {
|
||||||
|
/**
|
||||||
|
* InvalidRequestError
|
||||||
|
*/
|
||||||
|
400: InvalidRequestError
|
||||||
|
/**
|
||||||
|
* UnauthorizedError
|
||||||
|
*/
|
||||||
|
401: UnauthorizedError
|
||||||
|
/**
|
||||||
|
* SessionNotFoundError
|
||||||
|
*/
|
||||||
|
404: SessionNotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2SessionPermissionCreateError = V2SessionPermissionCreateErrors[keyof V2SessionPermissionCreateErrors]
|
||||||
|
|
||||||
|
export type V2SessionPermissionCreateResponses = {
|
||||||
|
/**
|
||||||
|
* Success
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
data: {
|
||||||
|
id: string
|
||||||
|
effect: PermissionV2Effect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2SessionPermissionCreateResponse =
|
||||||
|
V2SessionPermissionCreateResponses[keyof V2SessionPermissionCreateResponses]
|
||||||
|
|
||||||
|
export type V2SessionPermissionGetData = {
|
||||||
|
body?: never
|
||||||
|
path: {
|
||||||
|
sessionID: string
|
||||||
|
requestID: string
|
||||||
|
}
|
||||||
|
query?: never
|
||||||
|
url: "/api/session/{sessionID}/permission/{requestID}"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2SessionPermissionGetErrors = {
|
||||||
|
/**
|
||||||
|
* InvalidRequestError
|
||||||
|
*/
|
||||||
|
400: InvalidRequestError
|
||||||
|
/**
|
||||||
|
* UnauthorizedError
|
||||||
|
*/
|
||||||
|
401: UnauthorizedError
|
||||||
|
/**
|
||||||
|
* SessionNotFoundError | PermissionNotFoundError
|
||||||
|
*/
|
||||||
|
404: PermissionNotFoundError | SessionNotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2SessionPermissionGetError = V2SessionPermissionGetErrors[keyof V2SessionPermissionGetErrors]
|
||||||
|
|
||||||
|
export type V2SessionPermissionGetResponses = {
|
||||||
|
/**
|
||||||
|
* Success
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
data: PermissionV2Request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2SessionPermissionGetResponse = V2SessionPermissionGetResponses[keyof V2SessionPermissionGetResponses]
|
||||||
|
|
||||||
export type V2SessionPermissionReplyData = {
|
export type V2SessionPermissionReplyData = {
|
||||||
body: {
|
body: {
|
||||||
reply: PermissionV2Reply
|
reply: PermissionV2Reply
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { PermissionNotFoundError } from "@opencode-ai/protocol/errors"
|
import { PermissionNotFoundError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
import { response } from "../location"
|
import { response } from "../location"
|
||||||
|
|
||||||
function missingRequest(id: PermissionV2.ID) {
|
function missingRequest(id: PermissionV2.ID) {
|
||||||
@@ -20,6 +20,35 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
|
|||||||
return yield* response((yield* PermissionV2.Service).list())
|
return yield* response((yield* PermissionV2.Service).list())
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.handle(
|
||||||
|
"session.permission.create",
|
||||||
|
Effect.fn(function* (ctx) {
|
||||||
|
const permission = yield* PermissionV2.Service
|
||||||
|
return {
|
||||||
|
data: yield* permission
|
||||||
|
.ask({
|
||||||
|
id: ctx.payload.id,
|
||||||
|
sessionID: ctx.params.sessionID,
|
||||||
|
action: ctx.payload.action,
|
||||||
|
resources: ctx.payload.resources,
|
||||||
|
save: ctx.payload.save,
|
||||||
|
metadata: ctx.payload.metadata,
|
||||||
|
source: ctx.payload.source,
|
||||||
|
agent: ctx.payload.agent,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.catchTag(
|
||||||
|
"Session.NotFoundError",
|
||||||
|
(error) =>
|
||||||
|
new SessionNotFoundError({
|
||||||
|
sessionID: error.sessionID,
|
||||||
|
message: `Session not found: ${error.sessionID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
.handle(
|
.handle(
|
||||||
"session.permission.list",
|
"session.permission.list",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
@@ -27,6 +56,14 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
|
|||||||
return { data: yield* permission.forSession(ctx.params.sessionID) }
|
return { data: yield* permission.forSession(ctx.params.sessionID) }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.handle(
|
||||||
|
"session.permission.get",
|
||||||
|
Effect.fn(function* (ctx) {
|
||||||
|
const request = yield* (yield* PermissionV2.Service).get(ctx.params.requestID)
|
||||||
|
if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID)
|
||||||
|
return { data: request }
|
||||||
|
}),
|
||||||
|
)
|
||||||
.handle(
|
.handle(
|
||||||
"session.permission.reply",
|
"session.permission.reply",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user