Compare commits

...

1 Commits

Author SHA1 Message Date
Dax Raad 6efa25f8cd feat(server): add permission request endpoints 2026-06-24 23:43:49 -04:00
5 changed files with 287 additions and 2 deletions
@@ -814,6 +814,20 @@ const scenarios: Scenario[] = [
object(body.location)
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
.get("/api/session/{sessionID}/permission", "v2.session.permission.list")
.seeded((ctx) => ctx.session({ title: "Permission list owner" }))
@@ -822,6 +836,17 @@ const scenarios: Scenario[] = [
headers: ctx.headers(),
}))
.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
.get("/api/session/{sessionID}/question", "v2.session.question.list")
.seeded((ctx) => ctx.session({ title: "Question list owner" }))
+43 -1
View File
@@ -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 { Permission } from "@opencode-ai/schema/permission"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
import { Project } from "@opencode-ai/schema/project"
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.
.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(
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
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(
HttpApiEndpoint.post("session.permission.reply", "/api/session/:sessionID/permission/:requestID/reply", {
params: { sessionID: Session.ID, requestID: Permission.ID },
+92
View File
@@ -128,6 +128,7 @@ import type {
PermissionRespondResponses,
PermissionRuleset,
PermissionV2Reply,
PermissionV2Source,
ProjectCurrentErrors,
ProjectCurrentResponses,
ProjectDirectoriesErrors,
@@ -341,6 +342,10 @@ import type {
V2SessionListResponses,
V2SessionMessagesErrors,
V2SessionMessagesResponses,
V2SessionPermissionCreateErrors,
V2SessionPermissionCreateResponses,
V2SessionPermissionGetErrors,
V2SessionPermissionGetResponses,
V2SessionPermissionListErrors,
V2SessionPermissionListResponses,
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
*
+89
View File
@@ -12581,6 +12581,95 @@ export type 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 = {
body: {
reply: PermissionV2Reply
+38 -1
View File
@@ -4,7 +4,7 @@ import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { PermissionNotFoundError } from "@opencode-ai/protocol/errors"
import { PermissionNotFoundError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
import { response } from "../location"
function missingRequest(id: PermissionV2.ID) {
@@ -20,6 +20,35 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
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(
"session.permission.list",
Effect.fn(function* (ctx) {
@@ -27,6 +56,14 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
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(
"session.permission.reply",
Effect.fn(function* (ctx) {