Browse Source

feat(server): add permission request endpoints (#33774)

Dax 2 tháng trước cách đây
mục cha
commit
7f40aeab21

+ 25 - 0
packages/opencode/test/server/httpapi-exercise/index.ts

@@ -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
packages/protocol/src/groups/permission.ts

@@ -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 - 0
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -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 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -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
packages/server/src/handlers/permission.ts

@@ -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) {